ホームページ >Java >&#&チュートリアル >Java の 2 つの動的プロキシ: jdk と cglib によって生成されるプロキシのタイプとその実装方法
Java の 2 種類の動的プロキシとは何ですか?これらは、jdk プロキシと cglib です。今日は、Java の 2 つの動的プロキシについて説明し、最終的に生成されるプロキシ クラスはどのようなものになるのか、またそのプロキシを実装する方法について説明します。 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
は、次の必要があるクラスです。 add
(実際には AOP) を呼び出す前後にログを出力したいと考えています。最後に、同じインターフェイス ifc
を実装し、Real.add
の機能を実行するプロキシ クラスを生成する必要がありますが、print ステートメントの新しい行を追加する必要があります。これらすべてはユーザーにとって透過的であり、ユーザーはインターフェイス呼び出しのみを気にする必要があります。 Real.add
の周りに追加のコードを追加するには、🎜メソッド インターセプター🎜 に似たものを通じて動的プロキシが実装されます。Java プロキシでは、これは InvocationHandler
code> です。 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); }🎜 ここで最も重要なのは、実際には、プロキシ クラスの
add
メソッドと他のメソッドです (インターフェイスで他のメソッドも定義されている場合)。 )、最後に、この Handler
の invoke
メソッドを呼び出すだけです。通常、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
メソッドを直接呼び出し、3 つのパラメータを渡します。最初のパラメータは、プロキシ クラス自体の this ポインタです。 2 番目は add
メソッドのリフレクション クラス、3 番目はパラメータ リストです。そのため、invoke
メソッドでは、ユーザーがその動作を自由に定義して AOP を実装できます。このすべてのブリッジとなるのが InvocationHandler
であり、メソッドのインターセプトとプロキシを完了します。 🎜🎜プロキシ モードでは通常、プロキシ クラスが を呼び出すことができるように、プロキシ クラスが実際のクラス (プロキシ クラス) のインスタンス (この場合は <code>Real
のインスタンス) を持つ必要があります。 >Real オリジナルの add
メソッド。では、Real
はどこにあるのでしょうか?答えは InvocationHandler
にもあります。標準のエージェンシー モデルと比較すると、これには余分な入れ子層があるように見えますが、エージェンシー チェーンを構築できる限り、エージェンシー モデルの要件を満たしています。 🎜🎜ここでは、add
メソッドのリフレクション インスタンス mAdd
の初期化メソッドに注目してください。静的ブロック static {...}
を使用しています。一度設定すれば、マルチスレッドの問題は発生しません。もちろん、遅延読み込みやその他の方法を使用することもできますが、同時実行の安全性を考慮する必要があります。 🎜🎜最後に、JDK プロキシ
の具体的な使用方法を見てみましょう: 🎜Object re = proxy.invokeSuper(obj, args);🎜 メソッド
newProxyInstance
は、プロキシ クラスを動的に生成し、ifc インターフェイス。このメソッドには 3 つのパラメータが必要です。最初の ClassLoader は重要ではありません。2 番目のパラメータは、JDK のプロキシが完全にインターフェースに基づいており、代わりにインターフェースのメソッドをカプセル化するため、このプロキシ クラスが実装する必要があるインターフェースのリストです。 Entity クラスの 3 番目のパラメータは 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
のソース コードは、原理を説明するためだけに参照用に最も合理化されたバージョンにすぎず、JDK プロキシ
ではないということです。 code> と cglib
は実際にプロキシ クラスを生成します。実際のプロキシ クラスのロジックはさらに複雑ですが、原理は基本的に同じです。さらに、前述したように、実際にはソース コードを生成するのではなく、クラスのバイトコードを直接生成します。たとえば、cglib
は ASM
をカプセル化し、クラス データを直接生成します。 。 ProxyClass
的源代码,仅是参考性的最精简版本,只是为了说明原理,而不是JDK Proxy
和cglib
真正生成的代理类的样子,真正的代理类的逻辑要复杂的多,但是原理上基本是一致的。另外之前也说到过,事实上它们也不会生成源码,而是直接产生类的字节码,例如cglib
是封装了ASM
来直接生成Class数据的。
前面了解了代理类是什么,接下来就要介绍如何生成代理类,我结合资料整理了两个方案:
第一种方法是动态生成ProxyClass
源码,然后动态编译,就能得到Class了。这里就需要利用反射,加上一系列字符串拼接,生成源码。如果你充分理解代理类应该长什么样,其实并不是很难做到。那如何动态编译呢?你可以使用JOOR,这是一个封装了javax.tools.JavaCompiler
的库,帮助你方便地实现动态编译Java源代码。我试着写了一个Demo,纯粹是实验性质的。而且它有个重大问题,我不知道如何修改它编译使用的classpath,在默认情况下它无法引用到你自己定义的任何类,因为它们不在编译的classpath里,编译就不会通过,这实际上就使得这个代码生成器没有任何卵用。。。我强行通过修改System.setProperty
的classpath
来添加我的class路径绕开了这个问题,然而这显然不是个解决根本问题的方法。
第二种方法更直接,就是生成类的字节码。这也是cglib
プロキシ クラスとは何かを学習したので、次に、2 つのプロキシ クラスを生成する方法を紹介します。解決策:
最初の方法は、ProxyClass
ソース コードを動的に生成し、それを動的にコンパイルしてクラスを取得することです。ここでは、リフレクションを使用し、一連の文字列のスプライシングを追加してソース コードを生成する必要があります。プロキシ クラスがどのようなものであるべきかを完全に理解していれば、これはそれほど難しいことではありません。では、動的にコンパイルするにはどうすればよいでしょうか? JOOR は、javax.tools.JavaCompiler
をカプセル化するライブラリであり、Java ソース コードを簡単に動的にコンパイルするのに役立ちます。純粋に実験的なデモを作成してみました。そして、これには大きな問題があります。コンパイルに使用するクラスパスを変更する方法がわかりません。デフォルトでは、自分で定義したクラスはコンパイルされたクラスパスに含まれていないため、参照できません。これは実際には、このコード ジェネレーターを役に立たなくします。 。 。 System.setProperty
の classpath
を変更してクラスパスを追加することで、この問題を強制的に回避しました。ただし、これは明らかに根本的な問題の解決策ではありません。
2 番目の方法はより直接的で、クラスのバイトコードを生成します。これは、cglib
によって使用されるメソッドでもあり、クラス データを直接操作するために使用できるライブラリである ASM をカプセル化します。これにより、必要なクラスを生成または変更できます。これには、仮想マシンのバイトコードをよく理解している場合にのみ、この比較的ブラックなテクノロジ ルーチンを習得できる必要があります。ここにデモも書きました。これは純粋に実験です。興味のあるお子様は自分で試してみることもできます。バイトコードの作成はアセンブリに似ていますが、実際にはアセンブリよりもはるかに簡単です。これは、レジスタとメモリ アドレス、ヒープとスタック、さまざまな変数とアドレスが飛び交うアセンブリとは異なります。バイトコードの実行方法は非常に明確であり、変数はローカル変数テーブルに格納され、スタックは関数呼び出しにのみ使用されるため、非常に直感的です。 関連記事:
以上がJava の 2 つの動的プロキシ: jdk と cglib によって生成されるプロキシのタイプとその実装方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。