자바 와 모드 - 동적 에이전트 모드
22591 단어 자바
프 록 시 모드: 다른 대상 에 게 대역 이나 자리 표시 자 를 제공 하여 이 대상 에 대한 접근 을 제어 합 니 다.프 록 시 모드 를 사용 하여 프 록 시 대상 을 만 들 고 프 록 시 대상 이 특정한 대상 의 접근 을 제어 하도록 합 니 다. 프 록 시 대상 은 원 격 대상 이나 안전 통제 가 필요 한 대상 일 수 있 습 니 다.나 는 C 언어 에서 포인터 가 하나의 대리 대상 이 고 데 이 터 를 진정 으로 저장 하 는 것 은 메모리 의 다른 구역 이 라 고 생각한다.자바 는 API 를 제공 하여 동적 프 록 시 를 생 성 할 수 있 기 때문에 표준 프 록 시 모드 를 정적 프 록 시 모드 라 고 부른다.정적 에이전트 모델 에서 세 가지 역할 이 있 는데 이 세 가지 역할 을 이해 하면 에이전트 모델 을 이해 할 수 있다.
a. 추상 적 인 대상: 실제 캐릭터 와 추상 적 인 캐릭터 의 공공 인터페이스 (클래스 일 수도 있 고 인터페이스 일 수도 있다) Subject 를 정의 했다.
b. 대리 역할: 대리 역할 내부 에 실제 캐릭터 에 대한 인용 이 포함 되 어 있 고 이 인용 을 통 해 실제 캐릭터 가 완성 하고 자 하 는 임 무 를 수행 합 니 다.그 밖 에 대리 역할 은 다른 기능 을 완성 할 수 있다.
c. 실제 역할: 실제 임 무 를 완성 해 야 하 는 역할 은 우리 가 시종 인용 해 야 할 대상 이다.
대리 역할 과 실제 역할 은 모두 추상 적 인 역할 을 실현 했다.
코드 를 써 서 예 를 들 면:
// ,
interface Subject {
public void request();
}
//
class RealSubject implements Subject {
public void request() {
do something;
}
}
// ,
class ProxySubject implements Subject {
private RealSubject sub;
public ProxySubject(RealSubject obj) {
this.sub = obj;//
}
public void request() {
sub.request();//
}
}
public static void main(String[] args) {
Subject proxy = new ProxySubject();
proxy.request();
}
프 록 시 모델 은 여러 가지 변형 이 있 습 니 다. 이것 은 연구 할 만 한 가치 가 있 지만 오늘 제 중점 은 자바 의 동적 대리 입 니 다.
동적 에이전트
위의 프 록 시 모드 에 문제 가 있 습 니 다. 그것 은 서로 다른 인터페이스 에 프 록 시 클래스 를 따로 제공 해 야 한 다 는 것 입 니 다.현재 이런 문제 가 있 습 니 다. 시스템 에 10 가지 방법 이 호출 되 기 전에 안전 검증 을 해 야 합 니 다. 이 10 가지 유형 은 각각 다른 인 터 페 이 스 를 실현 합 니 다.이런 상황 은 바로 대리 모델 이 역할 을 발휘 하 는 곳 이다. 진정한 방법 이 호출 되 기 전에 현재 대리 류 에서 안전 검증 을 하고 통과 되면 진정한 방법 을 호출 하고 그렇지 않 으 면 돌아간다.이렇게 하면 안전 검증 의 목적 을 달성 할 뿐만 아니 라 개폐 원칙 도 위반 하지 않 는 다.그러나 안전 검증 은 모두 똑 같 습 니 다. 우 리 는 정말 10 개의 대리 류 를 실현 한 다음 에 똑 같은 안전 검증 을 해 야 합 니까?코드 를 반복 하 는 것 은 나 쁜 기운 을 의미 하 는 거 야.자바 의 동적 대 리 는 우리 가 이 일 을 해결 하 는 데 도움 을 줄 수 있다.동적 대 리 는 말 그대로 동적 으로 대리 류 를 생 성하 고 대리 서 비 스 를 하 는 것 이다.자바 의 동적 대 리 를 보기 전에 동적 대 리 를 실현 하려 면 어떤 조건 이 필요 한 지 분석 해 보 세 요.
(1) 프 록 시 모드 에서 클 라 이언 트 는 실제 적 으로 프 록 시 역할 과 접촉 하고 있다. 이 는 실제 대상 이 라 고 생각 하지만 사실은 프 록 시 대상 이다. 그러나 이런 프 록 시 는 클 라 이언 트 에 대해 투명 하 다. 클 라 이언 트 는 인 터 페 이 스 를 대상 으로 프로 그래 밍 을 하 는데 추상 적 인 인터페이스 만 있 고 구체 적 인 실현 유형 을 모른다.그래서 우 리 는 추상 적 인 인 터 페 이 스 를 정의 해 야 한다. 실제 역할 과 대리 역할 은 모두 추상 적 인 인 터 페 이 스 를 실현 하면 클 라 이언 트 에 대한 투명 한 대 리 를 실현 할 수 있다.
(2) 동적 대리 도 대리 일 뿐이다. 대 리 는 진정한 방법 이 집행 되 기 전이 나 그 후에 논 리 를 추가 하 는 것 이 아니 라 주요 한 업무 논 리 는 실제 역할 이 실현 되 어야 하기 때문에 반드시 실제 역할 이 있어 야 한다.이것 은 정적 에이전트 모드 와 같 습 니 다.우 리 는 여전히 업무 논리 에 따라 RealSubject 를 정의 해 야 한다.
(3) 동적 에이전트 역시 에이전트 류 가 필요 합 니 다. 정적 에이전트 와 의 차 이 는 정적 에이전트 류 는 프로그래머 에 의 해 생 성 되 고 동적 에이전트 류 는 동적 으로 생 성 됩 니 다.그러나 동적 대리 류 의 성질 은 일반적인 대리 류 와 같다. 모두 a. 추상 적 인 역할 인 터 페 이 스 를 실현 해 야 한다. 이것 은 동적 으로 생 성 된 것 이기 때문에 어떤 인 터 페 이 스 를 실현 해 야 할 지 모 르 기 때문에 실제 역할 이 실현 하 는 모든 인 터 페 이 스 를 실현 해 야 한다.b. 캐릭터 의 인용 저장 하기;
앞의 두 가지 조건 은 정적 에이전트 모델 과 같 습 니 다. 우 리 는 관심 을 가 질 필요 가 없습니다. 우리 가 알 아야 할 문 제 는 동적 에이전트 류 에 필요 한 인터페이스 와 그것 이 어떻게 대리 방법 으로 호출 되 는 지 하 는 것 입 니 다.
동적 프 록 시 클래스 생 성
동적 프 록 시 를 실현 하기 위해 자바 는 프 록 시 클래스 와 InvocationHandler 인 터 페 이 스 를 제공 합 니 다. 프 록 시 클래스 는 동적 프 록 시 클래스 를 생 성 하 는 데 사 용 됩 니 다. InvocationHandler 인 터 페 이 스 는 프 록 시 클래스 가 실제로 발생 하 는 곳 입 니 다. 퍼 블 릭 Object invoke (Object proxy, Method method, Object [] args) throws Throwable, 프 록 시 클래스 의 모든 방법 호출 은 invoke () 방법 으로 전 환 됩 니 다.다음 에 우 리 는 이 방법 을 말 하고 있 습 니 다. 먼저 프 록 시 류 가 동적 프 록 시 류 를 어떻게 생 성 하 는 지 보 겠 습 니 다.Proxy 는 다음 과 같은 방법 을 제공 합 니 다.
// proxy InvocationHandler
static InvocationHandler getInvocationHandler(Object proxy)
// loader , interfaces
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
// cl
static boolean isProxyClass(Class<?> cl)
// loader , interfaces , InvocationHandler h
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
프 록 시가 제공 한 네 가지 방법 은 모두 static 이 고 사실은 프 록 시 팩 토리 라 고 해 야 한 다 는 것 을 알 수 있다.상기 방법 에 대한 구체 적 인 내용 은 자바 독 을 볼 수 있 지만 동적 에이전트 의 실현 을 잘 보기 위해 프 록 시의 소스 코드 를 연구 하기 로 했 습 니 다.편폭 을 고려 할 때 일부 소스 코드 만 선택 했다. class Proxy implements java.io.Serializable {
/* :$Proxy , nextUniqueNumber++*/
private static long nextUniqueNumber = 0;
private final static String proxyClassNamePrefix = "$Proxy";
/** */
private final static Class[] constructorParams = { InvocationHandler.class };
/* ClassLoader */
private static Map<ClassLoader, Map<List<String>, Object>> loaderToCache
= new WeakHashMap<>();
/** */
private static Object pendingGenerationMarker = new Object();
/** next number to use for generation of unique proxy class names */
private static Object nextUniqueNumberLock = new Object();
/** , proxyClasses */
private static Map<Class<?>, Void> proxyClasses =
Collections.synchronizedMap(new WeakHashMap<Class<?>, Void>());
/* InvocationHandler*/
protected InvocationHandler h;
/* */
protected Proxy(InvocationHandler h) {
doNewInstanceCheck();
this.h = h;
}
/* , loader , interfaces , getProxyClass0() */
public static Class<?> getProxyClass(ClassLoader loader,
Class<?>... interfaces)
throws IllegalArgumentException
{
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, interfaces);
}
return getProxyClass0(loader, interfaces);
}
/**
* :
* 1. 65535, Java
* 2. interfaces( ), interfaceNames, :
* interfaceNames Class , ,
* Class ,
* , (interfaceSet , )
* 3. loader loaderToCache map, , Map
* 4. List, 3 map :
* loader interfaces ,
* loader interface , , //point1
* , 5
* while(true) , point1
* 5. , interfaces non-public , non-public ,
* interfaces non-public , ;
* public , com.sun.proxy
* 6. :$Proxy
* 7. , proxyClasses, isProxy()
* 8. 3 map , point1 ,
* @param loader
* @param interfaces
* @return
*/
private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
// 65535, Java
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
//
Class<?> proxyClass = null;
/* */
String[] interfaceNames = new String[interfaces.length];
//
Set<Class<?>> interfaceSet = new HashSet<>();
// , 2
for (int i = 0; i < interfaces.length; i++) {
//
String interfaceName = interfaces[i].getName();
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(interfaceName, false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != interfaces[i]) {
throw new IllegalArgumentException(
interfaces[i] + " is not visible from class loader");
}
/* */
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/* */
if (interfaceSet.contains(interfaceClass)) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
interfaceSet.add(interfaceClass);
interfaceNames[i] = interfaceName;
}
/* List, key map , */
List<String> key = Arrays.asList(interfaceNames);
/* loader map*/
Map<List<String>, Object> cache;
synchronized (loaderToCache) {
cache = loaderToCache.get(loader);
if (cache == null) {
cache = new HashMap<>();
loaderToCache.put(loader, cache);
}
}
synchronized (cache) {
/**
* while(true) :
* (1) , ,
* (2) , , , (1),
* (3) , , , , (1)
*/
do {
Object value = cache.get(key);
if (value instanceof Reference) {
proxyClass = (Class<?>) ((Reference) value).get();
}
if (proxyClass != null) {
return proxyClass;
} else if (value == pendingGenerationMarker) {
try {
cache.wait();
} catch (InterruptedException e) {
}
continue;
} else {
cache.put(key, pendingGenerationMarker);
break;
}
} while (true);
}
try {
//
String proxyPkg = null;
/* non-public , non-public , com.sun.proxy*/
for (int i = 0; i < interfaces.length; i++) {
int flags = interfaces[i].getModifiers();
if (!Modifier.isPublic(flags)) {
String name = interfaces[i].getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
{
/* , $Proxy */
long num;
synchronized (nextUniqueNumberLock) {
num = nextUniqueNumber++;
}
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/* */
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces);
try {
proxyClass = defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}
// proxyClasses ,isProxy()
proxyClasses.put(proxyClass, null);
} finally {
/* */
synchronized (cache) {
if (proxyClass != null) {
cache.put(key, new WeakReference<Class<?>>(proxyClass));
} else {
cache.remove(key);
}
cache.notifyAll();
}
}
return proxyClass;
}
/**
*
* 1. getProxyClass0() Class
* 2. Constructor
* 3. Constructor.newInstance()
* InvocationHandler
*/
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
// loader , interfaces
Class<?> cl = getProxyClass0(loader, interfaces);
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
return newInstance(cons, ih);
}
// , ,
private static Object newInstance(Constructor<?> cons, InvocationHandler h) {
return cons.newInstance(new Object[] {h} );
}
/* cl , proxyClasses , */
public static boolean isProxyClass(Class<?> cl) {
if (cl == null) {
throw new NullPointerException();
}
return proxyClasses.containsKey(cl);
}
/* proxy InvocationHandler, proxy.h, */
public static InvocationHandler getInvocationHandler(Object proxy)
throws IllegalArgumentException
{
final Proxy p = (Proxy) proxy;
final InvocationHandler ih = p.h;
return ih;
}
}
프 록 시가 프 록 시 클래스 를 생 성 할 때 중요 한 작업 은 프 록 시 클래스 가 필요 한 인 터 페 이 스 를 실현 하도록 하 는 것 이다. 바이트 코드 를 생 성 하 는 작업 은 프 록 시 Generator 가 한다.동적 에이전트 클래스 를 생 성 할 때, 우 리 는 클래스 캐리어, 실현 해 야 할 인터페이스, 그리고 이와 관련 된 InvocationHandler 대상 에 게 전달 해 야 한다. InvocationHandler 대상 이 해 야 할 일 은 바로 우리 가 실현 하고 자 하 는 에이전트 논리 이다. 예 를 들 어, 우 리 는 모든 방법 을 사용 하기 전에 안전 검 사 를 하고 싶다. 그러면 InvocationHandler 의 invoke ()방법 에서 안전 검사 의 논 리 를 호출 합 니 다. invoke () 는 대리 가 발생 하 는 곳 입 니 다.invoke () 방법 을 살 펴 보 겠 습 니 다.public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
其中proxy是代理类,不用去管它。method是要真实对象要执行的方法,也就是我们要代理的方法,args是method的参数,所以如果代理需要做安全检查的话,可以这么写:public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if( ) method.invoke(obj,args); else return null; }
예 를 들 어 이렇게 많은 것 을 말 했 는데 우 리 는 동적 대 리 를 실현 하 는 예 입 니 다. 논 리 는 이 렇 습 니 다. 세 개의 모듈 이 있 는데 그것 이 바로 추가, 삭제, 수정 입 니 다. 이 코드 들 은 이미 완성 되 었 습 니 다.실제 작업 을 하기 전에 안전 검증 이 통과 되면 해당 하 는 방법 을 사용 해 야 한다. 그렇지 않 으 면 이번 작업 이 실패 할 것 이다.먼저 각 인터페이스 와 실현 을 정의 합 니 다.안전 검사 모듈// interface Add { public void add(String s); } class AddImp implements Add { public void add(String s) { System.out.println(s + " "); } } // interface Delete { public void delete(String s); } class DeleteImp implements Delete { public void delete(String s) { System.out.println(s + " "); } } // interface Update { public void update(String s); } class UpdateImp implements Update { public void update(String s) { System.out.println(s + " "); } }
동적 대 리 를 생 성하 고 대리 집행:/** * , */ class SafeCheck { static Random rand = new Random(25); public static boolean check(Object obj) { if(rand.nextInt(20) > 10) return false; return true; } }
테스트 코드:/** * , * * Proxy , */ class DynamicProxy implements InvocationHandler { // , Object private Object originalObj; /** * , originalObj */ public Object bind(Object originalObj) { this.originalObj = originalObj; //originalObj Class Class<?> clazz = originalObj.getClass(); // , this return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this); } /** * method , null * , * ,proxy ,method , originalObj */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // , if(SafeCheck.check(originalObj)) return method.invoke(originalObj, args); else System.out.println(originalObj + " !"); // return null; } }
출력 결과:public class Client { public static void main(String[] args) { String name = "cxy"; DynamicProxy handler = new DynamicProxy(); // Add add = new AddImp(); Update update = new UpdateImp(); Delete delete = new DeleteImp(); // Add Add pa = (Add) handler.bind(add); // pa.add(name); // update Update pu = (Update)handler.bind(update); pu.update(name); // Delete Delete pd = (Delete)handler.bind(delete); pd.delete(name); } }
---- com.understanding.loaderandengine.AddImp@f0a3e8 ----- cxy ---- com.understanding.loaderandengine.UpdateImp@a22e0c ----- com.understanding.loaderandengine.UpdateImp@a22e0c ! ---- com.understanding.loaderandengine.DeleteImp@1b56bda ----- com.understanding.loaderandengine.DeleteImp@1b56bda !
시스템 에 세 개의 모듈 이 있 고 서로 다른 인 터 페 이 스 를 실현 하지만 그들 이 대리 가 필요 한 논리 가 같다 면 하나의 처리 프로그램 만 제시 하면 모든 모듈 을 동적 으로 대리 할 수 있다 는 것 을 알 수 있다. 이것 은 동적 대리 의 강력 한 점 이다.이 예 는 이미 AOP 의 맛 이 난다. 안전 검 사 는 원래 모듈 에 침입 하 는 방식 을 바 꾸 지 않 고 모든 모듈 에 서 비 스 를 제공 했다. Spring 의 AOP 중 하 나 는 동적 대 리 를 통 해 이 루어 진 것 이다.
동적 에이전트 의 실행
앞에서 동적 에이전트 류 가 어떻게 생 성 되 는 지 소개 하고 동적 에이전트 의 예 도 보 여 주 었 습 니 다. 사실 우 리 는 동적 에이전트 류 가 어떻게 대리 서 비 스 를 하 는 지 보고 싶 습 니 다. 그러면 동적 에이전트 류 의 코드 를 봐 야 합 니 다. 아래 의 도구 류 를 통 해 동적 대리 류 를 생 성 할 수 있 습 니 다.또는 main () 에 한 문장 을 추가 하여 프 록 시 클래스 를 생 성 합 니 다.import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Proxy; import sun.misc.ProxyGenerator; public class ProxyUtils { /* * , * clazz * params :clazz * proxyName : */ public static void generateClassFile(Class clazz,String proxyName) { // , byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces()); String paths = clazz.getResource(".").getPath(); System.out.println(paths); FileOutputStream out = null; try { // out = new FileOutputStream(paths+proxyName+".class"); out.write(classFile); out.flush(); } catch (Exception e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } }
역 컴 파일 을 통 해 대리 류 의 소스 코드 를 얻 었 습 니 다. 소스 코드 가 있 으 면 모든 것 이 우리 앞 에 노출 됩 니 다. 먼저 AddImp 에 생 성 된 대리 류 를 보 세 요.System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
이상 의 코드 는 Object 계승 을 위해 생 성 된 hashCode (), toString (), equals () 방법 을 삭 제 했 습 니 다. 이 어 Delete 프 록 시 류 의 소스 코드 를 살 펴 보고 선택 하 십시오./** * , Proxy , Add */ public final class AddProxy extends Proxy implements Add { private static Method m1; private static Method m3; private static Method m0; private static Method m2; // InvocationHandler , public AddProxy(InvocationHandler paramInvocationHandler) throws { super(paramInvocationHandler); } /** * Add add */ public final void add(String paramString) throws { try { // InvocationHandler invoke() this.h.invoke(this, m3, new Object[] { paramString }); return; } catch (Error|RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } static { try { // add Method , InvocationHandler invoke() , , m3 m3 = Class.forName("com.understanding.loaderandengine.Add").getMethod("add", new Class[] { Class.forName("java.lang.String") }); m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); return; } catch (NoSuchMethodException localNoSuchMethodException) { throw new NoSuchMethodError(localNoSuchMethodException.getMessage()); } catch (ClassNotFoundException localClassNotFoundException) { throw new NoClassDefFoundError(localClassNotFoundException.getMessage()); } } }
Update 의 코드 는 붙 이지 않 습 니 다. 위의 코드 에서 우 리 는 동적 에이전트 의 실행 과정 을 발견 할 수 있 습 니 다.// delete() public final void delete(String paramString) throws { try { this.h.invoke(this, m3, new Object[] { paramString }); return; } catch (Error|RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } // delte() Method m3 = Class.forName("com.understanding.loaderandengine.Delete").getMethod("delete", new Class[] { Class.forName("java.lang.String") });
(1) 동적 에이전트 클래스 는 InvocationHandler 의 인용 을 저장 해 야 합 니 다.
(2) 동적 에이전트 클래스 는 피 에이전트 클래스 가 실현 하 는 모든 인 터 페 이 스 를 실현 하고 해당 하 는 방법의 Method 대상 을 얻어 해당 하 는 방법 을 생 성 한다.
(3) 대 리 를 수행 할 때 동적 대리 류 는 사실 Invocation Handler. invoke () 방법 으로 넘 어 갔다. 이것 도 내 가 Invocation Handler 가 대리 역할 이 라 고 말 하 는 이유 이다.
여기 서 우 리 는 동적 대리 류 가 어떻게 생 성 되 고 동적 대리 가 어떻게 실현 되 는 지 알 게 되 었 고 작은 예 를 들 었 다. 이 작은 예 는 AOP 의 사상 이 있다.그런데 자바 에서 동적 에이 전 트 는 완벽 한 가요?프 록 시. getProxyclass) () 방법 을 돌 이 켜 보면 자바 의 동적 대 리 는 프 록 시 클래스 에 의 해 실 현 된 인터페이스 에 따라 대리 되 는 것 을 발견 할 수 있 습 니 다. 자바 의 동적 대 리 는 '인 터 페 이 스 를 위 한 대리' 라 고 할 수 있 습 니 다. 만약 에 하나의 클래스 가 인 터 페 이 스 를 실현 하지 않 으 면 동적 대리 류 를 생 성 할 수 없습니다. 이것 은 결함 이지 만 어 쩔 수 없 이 말 할 수 있 습 니 다.자바 의 동적 대 리 는 여전히 매우 강하 다.
전재 설명: 홍엽 을 비유 하 다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Is Eclipse IDE dying?In 2014 the Eclipse IDE is the leading development environment for Java with a market share of approximately 65%. but ac...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.