Java의 두 가지 동적 프록시 유형은 무엇인가요? 그것들은 jdk 프록시와 cglib입니다. 오늘 우리는 Java에서 두 동적 프록시의 최종 생성된 프록시 클래스가 어떻게 생겼는지와 프록시를 구현하는 방법에 대해 논의할 것입니다. apache php mysql
Java 인터뷰에 참여해 본 친구들은 면접관들이 Spring AOP가 어떻게 구현되는지 등의 질문을 좋아한다는 것을 알고 계실 것 같아서, 질문을 정리해서 이 글을 썼습니다. 여기서는 AOP와 프록시 모드의 개념을 자세히 설명하지 않습니다. 즉, AOP 구현 방법인 동적 프록시에 대해서만 직접 이야기하겠습니다. 정적 프록시와 비교하여 동적 프록시는 런타임에 Java 프록시 클래스를 동적으로 생성하고 프록시 클래스는 특정 메소드의 캡슐화를 완료하고 AOP 기능을 실현합니다.
저의 개인적인 배치와 생각입니다. 결과는 실제 jdk 및 cglib에서 생성된 것과 동일하지 않을 수 있지만 기본적으로는 동일합니다.
기사 마지막 부분에서는 간단한 동적 프록시를 직접 구현하는 방법에 대해서도 논의하고, 물론 참조용으로만 직접 구현한 간단한 버전을 제공할 것입니다.
Java 리플렉션 패키지 java.lang.reflect
에서 제공하는 동적 프록시 메서드입니다. 이 프록시 메서드는 완전히 인터페이스를 기반으로 합니다. 먼저 간단한 예를 들어보겠습니다. java.lang.reflect
提供的动态代理的方式,这种代理方式是完全基于接口的。这里先给出一个简单的例子。
定义接口:
interface ifc { int add(int, int); }
然后是接口ifc
的实现类Real
:
class Real implements ifc { @Override public int add(int x, int y) { return x + y; }
Real
就是我们需要代理的类,比如我们希望在调用add
的前后打印一些log,这实际上就是AOP了。我们需要最终产生一个代理类,实现同样的接口ifc
,执行Real.add
的功能,但需要增加一行新的打印语句。这一切对用户是透明的,用户只需要关心接口的调用。为了能在Real.add
的周围添加额外代码,动态代理都是通过一种类似方法拦截器的东西来实现的,在Java Proxy里这就是InvocationHandler
.
class Handler implements InvocationHandler { private final Real real; public Handler(Real real) { this.real = real; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { System.out.println("=== BEFORE ==="); Object re = method.invoke(real, args); System.out.println("=== AFTER ==="); return re; } }
这里最关键的就是invoke
方法,实际上代理类的add
方法,以及其它方法(如果接口还定义了其它方法),最终都只是调用这个Handler
的invoke
方法,由你来具体定义在invoke里需要做什么,通常就是调用真正实体类Real
的方法,这里就是add
,以及额外的AOP行为(打印 BEFORE 和 AFTER)。所以可想而知,代理类里必然是有一个InvocationHandler
的实例的,所有的接口方法调用都会由这个handler实例来代理。
所以我们应该能大概刻画出这个代理类的模样:
public ProxyClass implements ifc { private static Method mAdd; private InvocationHandler handler; static { Class clazz = Class.forName("ifc"); mAdd = clazz.getMethod("add", int.class, int.class); } @Override public int add(int x, int y) { return (Integer)handler.invoke(this, mAdd, new Object[] {x, y}); } }
这个版本非常简单,但已足够实现我们的要求。我们来观察这个类,首先毋庸置疑它实现了ifc
接口,这是代理模式的根本。它的add
方法直接调用InvocationHandler
实例的invoke
方法,传入三个参数,第一个是代理类本身this指针,第二个是add
方法的反射类,第三个是参数列表。所以在invoke
方法里,用户就能自由定义它的行为实现AOP,所有这一切的桥梁就是InvocationHandler
,它完成方法的拦截与代理。
代理模式一般要求代理类中有一个真正类(被代理类)的实例,在这里也就是Real
的实例,这样代理类才能去调用Real
中原本的add
方法。那Real
在哪里呢?答案也是在InvocationHandler
里。这与标准的代理模式相比,似乎多了一层嵌套,不过这并没有关系,只要这个代理的链条能够搭建起来,它就符合代理模式的要求。
注意到这里add
方法的反射实例mAdd
的初始化方式,我们使用静态块static {...}
来完成,只会被设置一次,并且不会有多线程问题。当然你也可以用懒加载等方式,不过就得考虑并发的安全性。
最后看一下JDK Proxy
的具体使用:
Handler handler = new Handler(new Real()); ifc p = (ifc)Proxy.newProxyInstance(ifc.class.getClassLoader(), new Class[] {ifc}, handler); p.add(1, 2);
方法newProxyInstance
就会动态产生代理类,并且返回给我们一个实例,实现了ifc
接口。这个方法需要三个参数,第一个ClassLoader并不重要;第二个是接口列表,即这个代理类需要实现那些接口,因为JDK的Proxy是完全基于接口的,它封装的是接口的方法而不是实体类;第三个参数就是InvocationHandler
的实例,它会被放置在最终的代理类中,作为方法拦截和代理的桥梁。注意到这里的handler
包含了一个Real
class Real { public int add(int x, int y) { return x + y; } }
ifc
의 구현 클래스 Real
: 🎜public class Interceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("=== BEFORE ==="); Object re = proxy.invokeSuper(obj, args); System.out.println("=== AFTER ==="); return re; } }
Real
은 우리가 필요한 클래스입니다. 우리는 실제로 AOP인 add
를 호출하기 전후에 일부 로그를 인쇄하기를 희망합니다. 마침내 동일한 인터페이스 ifc
를 구현하고 Real.add
의 기능을 수행하는 프록시 클래스를 생성해야 하지만 새로운 print 문 줄을 추가해야 합니다. 이 모든 것은 사용자에게 투명하며 사용자는 인터페이스 호출에만 관심을 가지면 됩니다. Real.add
주변에 추가 코드를 추가하기 위해 🎜메소드 인터셉터🎜와 유사한 것을 통해 동적 프록시가 구현됩니다. Java 프록시에서는 InvocationHandler
코드>입니다.🎜 public static void main(String[] args) { Enhancer eh = new Enhancer(); eh.setSuperclass(Real.class); eh.setCallback(new Interceptor()); Real r = (Real)eh.create(); int result = r.add(1, 2); }
invoke
메서드입니다. 실제로 프록시 클래스의 add
메서드와 다른 메서드도 있습니다(인터페이스가 다른 메서드도 정의하는 경우). ), 마지막으로 이 Handler
의 invoke
메서드를 호출합니다. 일반적으로 호출에서 수행해야 할 작업을 구체적으로 정의하는 것은 사용자에게 달려 있습니다. 실제 엔터티 클래스 Real
. 이 경우 add
및 추가 AOP 동작(BEFORE 및 AFTER 인쇄). 따라서 프록시 클래스에 InvocationHandler
인스턴스가 있어야 하고 모든 인터페이스 메서드 호출이 이 핸들러 인스턴스에 의해 프록시되는 것이 가능합니다. 🎜🎜따라서 이 프록시 클래스가 어떻게 생겼는지 대략적으로 설명할 수 있어야 합니다. 🎜public ProxyClass extends Real { private static Method mAdd; private static MethodProxy mAddProxy; private MethodInterceptor interceptor; static { Class clazz = Class.forName("ifc"); mAdd = clazz.getMethod("add", int.class, int.class); // Some logic to generate mAddProxy. // ... } @Override public int add(int x, int y) { return (Integer)interceptor.invoke( this, mAdd, new Object[] {x, y}, mAddProxy); } }
ifc
인터페이스가 구현되어 있다는 점은 의심할 여지가 없습니다. add
메소드는 InvocationHandler
인스턴스의 invoke
메소드를 직접 호출하여 세 개의 매개변수를 전달합니다. 첫 번째는 프록시 클래스 자체의 this 포인터입니다. , 두 번째는 add
메소드의 리플렉션 클래스이고, 세 번째는 매개변수 목록입니다. 따라서 invoke
메소드에서 사용자는 AOP를 구현하기 위한 동작을 자유롭게 정의할 수 있습니다. 이 모든 것에 대한 브리지는 메소드의 차단 및 프록시를 완료하는 InvocationHandler
입니다. 🎜🎜프록시 모드에서는 일반적으로 프록시 클래스에 실제 클래스(프록시 클래스)의 인스턴스가 있어야 하며, 이 경우 프록시 클래스가 를 호출할 수 있도록 <code>Real
의 인스턴스입니다. >Real 원래의 add
메소드입니다. 그렇다면 진짜
는 어디에 있나요? 대답은 InvocationHandler
에도 있습니다. 표준 에이전시 모델과 비교하면 추가 중첩 계층이 있는 것처럼 보이지만 에이전시 체인을 구축할 수 있는 한 에이전시 모델의 요구 사항을 충족하는 것은 중요하지 않습니다. 🎜🎜여기서 add
메서드의 반사 인스턴스 mAdd
의 초기화 메서드에 주목하세요. 정적 블록 static {...}
을 사용합니다. 한 번만 설정하면 멀티스레딩 문제가 발생하지 않습니다. 물론 지연 로딩이나 다른 방법을 사용할 수도 있지만 동시성의 안전성을 고려해야 합니다. 🎜🎜마지막으로 JDK 프록시
의 구체적인 사용을 살펴보겠습니다. 🎜Object re = proxy.invokeSuper(obj, args);
newProxyInstance
메서드는 프록시 클래스를 동적으로 생성하고 인스턴스를 반환합니다. code>ifc 인터페이스. 이 메소드에는 세 개의 매개변수가 필요합니다. 두 번째는 인터페이스 목록, 즉 이 프록시 클래스가 구현해야 하는 인터페이스입니다. 왜냐하면 JDK의 프록시는 완전히 인터페이스를 기반으로 하고 대신 인터페이스의 메소드를 캡슐화하기 때문입니다. Entity 클래스의 세 번째 매개변수는 InvocationHandler
의 인스턴스이며, 이는 메서드 차단과 프록시 사이의 브리지로 최종 프록시 클래스에 배치됩니다. 여기서 handler
에는 위에서 언급한 것처럼 프록시 모드의 필수 요구 사항인 Real
인스턴스가 포함되어 있습니다. 🎜总结一下JDK Proxy
的原理,首先它是完全面向接口的,其实这才是符合代理模式的标准定义的。我们有两个类,被代理类Real
和需要动态生成的代理类ProxyClass
,都实现了接口ifc
。类ProxyClass
需要拦截接口ifc
上所有方法的调用,并且最终转发到实体类Real
上,这两者之间的桥梁就是方法拦截器InvocatioHandler
的invoke
方法。
上面的例子里我给出类ProxyClass
的源代码,当然实际上JDK Proxy
是不会去产生源代码的,而是直接生成类的原始数据,它具体是怎么实现我们暂时不讨论,我们目前只需要关心这个类是什么样的,以及它实现代理的原理。
这是Spring
使用的方式,与JDK Proxy
不同之处在于它不是面向接口的,而是基于类的继承。这似乎是有点违背代理模式的标准格式,不过这没有关系,所谓的代理模式只是一种思想而不是严格的规范。我们直接看它是如何使用的。
现在没有接口,我们直接有实体类:
class Real { public int add(int x, int y) { return x + y; } }
类似于InvocationHandler
,这里cglib
直接使用一个叫MethodInterceptor
的类,顾名思义。
public class Interceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("=== BEFORE ==="); Object re = proxy.invokeSuper(obj, args); System.out.println("=== AFTER ==="); return re; } }
使用方法:
public static void main(String[] args) { Enhancer eh = new Enhancer(); eh.setSuperclass(Real.class); eh.setCallback(new Interceptor()); Real r = (Real)eh.create(); int result = r.add(1, 2); }
如果你仔细和JDK Proxy
比较,会发现它们其实是类似的:
首先JDK Proxy
提供interface列表,而cglib
提供superclass供代理类继承,本质上都是一样的,就是提供这个代理类的签名,也就是对外表现为什么类型。
然后是一个方法拦截器,JDK Proxy
里是InvocationHandler
,而cglib
里一般就是MethodInterceptor
,所有被代理的方法的调用都是通过它们的invoke
方法进行转接的,AOP的逻辑也是在这一层实现。
它们不同之处上面已经说了,就在于cglib
生成的动态代理类是直接继承原始类的,所以我们这里也可以大概刻画出这个代理类长什么样子:
public ProxyClass extends Real { private static Method mAdd; private static MethodProxy mAddProxy; private MethodInterceptor interceptor; static { Class clazz = Class.forName("ifc"); mAdd = clazz.getMethod("add", int.class, int.class); // Some logic to generate mAddProxy. // ... } @Override public int add(int x, int y) { return (Integer)interceptor.invoke( this, mAdd, new Object[] {x, y}, mAddProxy); } }
因为直接继承了Real
,那自然就包含了Real
的所有public方法,都通过interceptor.invoke
进行拦截代理。这其实和上面JDK Proxy
的原理是类似的,连invoke
方法的签名都差不多,第一个参数是this指针代理类本身,第二个参数是方法的反射,第三个参数是方法调用的参数列表。唯一不同的是,这里多出一个MethodProxy
,它是做什么用的?
如果你仔细看这里invoke
方法内部的写法,当用户想调用原始类(这里是Real
)定义的方法时,它必须使用:
Object re = proxy.invokeSuper(obj, args);
这里就用到了那个MethodProxy
,那我们为什么不直接写:
Object re = method.invoke(obj, args);
答案当然是不可以,你不妨试一下,程序会进入一个无限递归调用。这里的原因恰恰就是因为代理类是继承了原始类的,obj
指向的就是代理类对象的实例,所以如果你对它使用method.invoke
,由于多态性,就会又去调用代理类的add
方法,继而又进入invoke
方法,进入一个无限递归:
obj.add() { interceptor.invoke() { obj.add() { interceptor.invoke() { ... } } } }
那我如何才能在interceptor.invoke()
里去调用基类Real
的add
方法呢?当然通常做法是super.add()
,然而这是在MethodInterceptor
的方法里,而且这里的method调用必须通过反射完成,你并不能在语法层面上做到这一点。所以cglib
封装了一个类叫MethodProxy
帮助你,这也是为什么那个方法的名字叫invokeSuper
,表明它调用的是原始基类的真正方法。它究竟是怎么办到的呢?你可以简单理解为,动态代理类里会生成这样一个方法:
int super_add(int x, int y) { return super.add(x, y); }
当然你并不知道有这么一个方法,但invokeSuper
会最终找到这个方法并调用,这都是在生成代理类时通过一系列反射的机制实现的,这里就不细展开了。
对比JDK Proxy
和cglib
动态代理的使用方法和实现上的区别,就会发现,它们本质上都差不多,都是提供两个最重要的东西:
接口列表或者基类,定义了代理类(当然也包括原始类)的签名。
一个方法拦截器,完成方法的拦截和代理,是所有调用链的桥梁。
위에 제공한 프록시 클래스 ProxyClass
의 소스 코드는 단지 원리를 설명하기 위한 참고용으로 가장 간소화된 버전일 뿐이라는 점에 유의해야 합니다. 실제 프록시 클래스의 논리는 훨씬 더 복잡하지만 원칙은 기본적으로 동일합니다. 또한 앞서 언급했듯이 실제로는 소스 코드를 생성하지 않고 클래스의 바이트코드를 직접 생성합니다. 예를 들어 cglib
는 ASM
을 캡슐화하여 클래스 데이터를 직접 생성합니다. . ProxyClass
的源代码,仅是参考性的最精简版本,只是为了说明原理,而不是JDK Proxy
和cglib
真正生成的代理类的样子,真正的代理类的逻辑要复杂的多,但是原理上基本是一致的。另外之前也说到过,事实上它们也不会生成源码,而是直接产生类的字节码,例如cglib
是封装了ASM
来直接生成Class数据的。
前面了解了代理类是什么,接下来就要介绍如何生成代理类,我结合资料整理了两个方案:
第一种方法是动态生成ProxyClass
源码,然后动态编译,就能得到Class了。这里就需要利用反射,加上一系列字符串拼接,生成源码。如果你充分理解代理类应该长什么样,其实并不是很难做到。那如何动态编译呢?你可以使用JOOR,这是一个封装了javax.tools.JavaCompiler
的库,帮助你方便地实现动态编译Java源代码。我试着写了一个Demo,纯粹是实验性质的。而且它有个重大问题,我不知道如何修改它编译使用的classpath,在默认情况下它无法引用到你自己定义的任何类,因为它们不在编译的classpath里,编译就不会通过,这实际上就使得这个代码生成器没有任何卵用。。。我强行通过修改System.setProperty
的classpath
来添加我的class路径绕开了这个问题,然而这显然不是个解决根本问题的方法。
第二种方法更直接,就是生成类的字节码。这也是cglib
프록시 클래스가 무엇인지 알아보았습니다.다음으로 프록시 클래스 생성 방법을 소개하겠습니다. . 정보를 바탕으로 정리하겠습니다. 두 가지 해결책이 제안되었습니다:
첫 번째 방법은 ProxyClass
소스 코드를 동적으로 생성한 다음 이를 동적으로 컴파일하여 가져오는 것입니다. 수업. 여기에서는 리플렉션을 사용하고 일련의 문자열 접합을 추가하여 소스 코드를 생성해야 합니다. 프록시 클래스가 어떻게 생겼는지 완전히 이해한다면 그렇게 어렵지 않습니다. 그렇다면 동적으로 컴파일하는 방법은 무엇입니까? javax.tools.JavaCompiler
를 캡슐화하는 라이브러리인 JOOR을 사용하면 Java 소스 코드를 동적으로 쉽게 컴파일할 수 있습니다. 나는 순전히 실험적인 데모를 작성하려고 했습니다. 그리고 여기에는 심각한 문제가 있습니다. 컴파일에 사용하는 클래스 경로를 수정하는 방법을 모르겠습니다. 기본적으로 컴파일된 클래스 경로에 없기 때문에 사용자가 직접 정의한 클래스를 참조할 수 없으며 컴파일이 통과되지 않습니다. 이것은 실제로 이 코드 생성기를 쓸모없게 만듭니다. . . 내 클래스 경로를 추가하기 위해 System.setProperty
의 classpath
를 수정하여 이 문제를 강제로 우회했습니다. 그러나 이것은 분명히 근본적인 문제에 대한 해결책은 아닙니다.
두 번째 방법은 더 직접적이며 클래스의 바이트코드를 생성하는 것입니다. 이는 cglib
에서 사용하는 방식이기도 합니다. Class 데이터를 직접 조작하는 데 사용할 수 있는 라이브러리인 ASM을 캡슐화합니다. 이를 통해 원하는 Class를 생성하거나 수정할 수 있습니다. 가상 머신의 바이트코드를 잘 이해하고 있는 경우에만 상대적으로 까다로운 이 기술 루틴을 마스터할 수 있습니다. 나는 또한 여기에 순전히 실험적인 데모를 작성했습니다. 관심 있는 어린이도 스스로 시도해 볼 수 있습니다. 바이트코드 작성은 어셈블리와 비슷하지만 실제로는 어셈블리보다 훨씬 쉽습니다. 레지스터와 메모리 주소, 힙과 스택, 다양한 변수와 주소가 돌아다니는 어셈블리와는 다릅니다. 바이트코드의 실행 방법은 매우 명확하며, 변수는 로컬 변수 테이블에 저장되며, 스택은 함수 호출에만 사용되므로 매우 직관적입니다. 관련 기사:
cglib 및 jdk의 두 가지 동적 프록시에 대한 자세한 설명
JDK 소개 및 cglib 세부 코드 설명#🎜🎜##🎜🎜#JDK 및 JRE 개요)-JAVA 초등 소개 영상 튜토리얼#🎜🎜 ##🎜🎜 #위 내용은 Java의 두 가지 동적 프록시: jdk 및 cglib에 의해 생성된 프록시 유형 및 구현 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!