Java中代理的理解及其實現方式(2)- 動態代理
上一節中我們介紹了Java中代理的概念及其靜態實現方式。
結尾處簡單說明了一下動態代理的概念:如果代理類是在程式執行時建立的就稱為動態代理。
也就是說,代理類並不是在Java程式碼中定義的,而是在程式碼執行時根據我們自己的需求動態生成的。也就是 說你想獲取哪個物件的代理,動態代理就會動態的為你生成這個物件的代理物件。這種方式顯然更加的靈活。
動態代理技術都是在框架中使用居多,例如:Struts1、Struts2、Spring和Hibernate等主流框架技術中都使用了動態代理技術。
其中Spring AOP 就是典型代表。
那麼在java中如果實現動態代理呢?
第一種,jdk中已經給我們提供了實現動態代理的類。
在這個包:java.lang.reflect.Proxy
看例子:
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;interface IA { void method1(String p);}class A implements IA { @Override public void method1(String p) { System.out.print("Hello "); System.out.println(p); } private void method2(String p) { System.out.print("Hello "); System.out.println(p); }}class JdkProxy { private IA a; public JdkProxy(IA a) { this.a = a; } public IA createJdkProxy() { return (IA) Proxy.newProxyInstance(a.getClass().getClassLoader(), a.getClass().getInterfaces(), (proxy, method, args) -> { System.out.println("request param : " + args[0].toString()); System.out.println("method start at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); method.invoke(a, args); System.out.println("method end at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); return null; }); }}
然後我們可以這麼呼叫:
public static void main(String[] args) { JdkProxy jdkProxy = new JdkProxy(new A()); IA aProxy = jdkProxy.createJdkProxy(); aProxy.method1("Java");}
其中JdkProxy類的createJdkProxy方法中使用了lamda的寫法。我們也可以用下面的寫法:
// 寫法二,不使用lamdaclass JdkProxy { private IA a; public JdkProxy(IA a) { this.a = a; } public IA createJdkProxy() { return (IA) Proxy.newProxyInstance(a.getClass().getClassLoader(), a.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("request param : " + args[0].toString()); System.out.println("method start at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); method.invoke(a, args); System.out.println("method end at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); return null; } }); }}// 寫法三,不使用匿名類class JdkProxy implements InvocationHandler { private IA a; public JdkProxy(IA a) { this.a = a; } public IA createJdkProxy() { return (IA) Proxy.newProxyInstance(a.getClass().getClassLoader(), a.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("request param : " + args[0].toString()); System.out.println("method start at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); method.invoke(a, args); System.out.println("method end at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); return null; }}
以上的寫法都可以,只是語法的區別。
// 核心程式碼就一行:Proxy.newProxyInstance(a.getClass().getClassLoader(), a.getClass().getInterfaces(), this);// 原型為:public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
三個引數如下:
a.getClass().getClassLoader()目標物件透過getClass方法獲取類的所有資訊後,呼叫getClassLoader() 方法來獲取類載入器。獲取類載入器後,可以透過這個型別的載入器,在程式執行時,將生成的代理類載入到JVM即Java虛擬機器中,以便執行時需要。a.getClass().getInterfaces()獲取被代理類的所有介面資訊,以便於生成的代理類可以具有代理類介面中的所有方法。InvocationHandler 這是呼叫處理器介面,它自定義了一個 invoke 方法,用於集中處理在動態代理類物件上的方法呼叫,通常在該方法中實現對被代理類方法的處理以及訪問。使用JDK的這種方式需要滿足以下條件:
被代理的類必須實現介面。從newProxyInstance方法的第二個引數即可知被代理的方法也一定要是實現了介面的方法jdk方式動態代理的原理大家可以參考對應的靜態代理中的第三種方式。
注:以後的章節中我們會講到Spring AOP 的@Transactional 註解,該註解有幾種不生效的場景,有幾種不生效的原因就在於此。
第二種,第三方包CGLib中也給我我們提供了實現動態代理的類。
在這個包:net.sf.cglib.proxy
看程式碼:
import net.sf.cglib.proxy.Enhancer;import net.sf.cglib.proxy.MethodInterceptor;// 這兩個包spring框架中也有// import org.springframework.cglib.proxy.Enhancer;// import org.springframework.cglib.proxy.MethodInterceptor;interface IA { void method1(String p);}class A implements IA { @Override public void method1(String p) { System.out.print("Hello "); System.out.println(p); } private void method2(String p) { System.out.print("Hello "); System.out.println(p); }}class CglibProxy { private IA a; public CglibProxy(IA a) { this.a = a; } public IA createCglibProxy() { return (IA) Enhancer.create(a.getClass(), (MethodInterceptor) (o, method, objects, methodProxy) -> { System.out.println("request param : " + objects[0].toString()); System.out.println("method start at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); method.invoke(a, objects); System.out.println("method end at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); return null; }); }}
然後我們可以這麼呼叫:
public static void main(String[] args) { CglibProxy cglibProxy = new CglibProxy(new A()); IA aProxy = cglibProxy.createCglibProxy(); aProxy.method1("Java"); }
同樣的 CglibProxy 類我們也可以寫成下面的兩種形式:
// 不是用lamdaclass CglibProxy { private IA a; public CglibProxy(IA a) { this.a = a; } public IA createCglibProxy() { return (IA) Enhancer.create(a.getClass(), new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("request param : " + objects[0].toString()); System.out.println("method start at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); method.invoke(a, objects); System.out.println("method end at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); return null; } }); }}// 不使用匿名類class CglibProxy implements MethodInterceptor { private IA a; public CglibProxy(IA a) { this.a = a; } public IA createCglibProxy() { return (IA) Enhancer.create(a.getClass(), this); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("request param : " + objects[0].toString()); System.out.println("method start at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); method.invoke(a, objects); System.out.println("method end at " + new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss:SSS").format(new Date())); return null; }}
以上寫法也只是語法的區別。
// 核心程式碼下面一行(IA) Enhancer.create(a.getClass(), this);// 原型public static Object create(Class type, Callback callback)
入參比較簡單,不再做說明了。
使用CGLib的這種方式需要滿足以下條件:
被代理的類不能是final的類被代理的方法必須是public 的,並且不能是final修飾的CGLib方式動態代理的原理大家可以參考對應的靜態代理中的第二種方式,使用的是類繼承的方式,因此需要滿足上面的條件。
注:Spring AOP 中使用的動態代理 會同時使用上面的兩種方式,Spring對JDK和CGLIB代理都做了實現,它會根據被代理類是否實現了介面而決定採用哪種動態代理方式。
如果被代理類實現了介面(比如我們大部分專案中的service 層,通常都會寫一個Service介面,再寫一個ServiceImpl類),就採用JDK動態代理;
如果沒介面,就採用CGLIB動態代理。
第三種,自己動手,使用反射和動態編譯建立代理類
直接看程式碼:
import javax.tools.JavaCompiler;import javax.tools.JavaFileObject;import javax.tools.SimpleJavaFileObject;import javax.tools.StandardJavaFileManager;import javax.tools.ToolProvider;import java.io.File;import java.io.IOException;import java.lang.reflect.Constructor;import java.net.URI;import java.util.ArrayList;import java.util.Arrays;import java.util.List;interface IA { void method1(String p);}class A implements IA { @Override public void method1(String p) { System.out.print("Hello "); System.out.println(p); } private void method2(String p) { System.out.print("Hello "); System.out.println(p); }}class DynamicProxy { public IA createProxyObj(IA a) { // 包名替換成你自己的 String packageName = "xxx.yyy"; String className = "B"; StringBuilder sb = new StringBuilder(); sb.append("package ") .append(packageName) .append(";\n") .append("import java.text.SimpleDateFormat;\n") .append("import java.util.Date;\n") .append("class ").append(className).append(" implements IA {\n") .append("\n") .append("private IA a;\n") .append("public ").append(className).append("() {\n") .append("}\n") .append("public ").append(className).append("(IA a) {\n") .append(" this.a = a;\n") .append("}\n") .append("\n") .append("@Override\n") .append("public void method1(String p) {\n") .append(" System.out.println(\"request param : \" + p);\n") .append(" System.out.println(\"method start at \" + new SimpleDateFormat(\"yyyy/MM/dd-HH:mm:ss:SSS\").format(new Date()));\n") .append(" a.method1(p);\n") .append(" System.out.println(\"method end at \" + new SimpleDateFormat(\"yyyy/MM/dd-HH:mm:ss:SSS\").format(new Date()));\n") .append("}\n") .append("}\n"); String absolutePath = new File(DynamicProxy.class.getClassLoader() .getResource("").getFile()).getAbsolutePath(); List<String> options = new ArrayList<String>(); options.add("-d"); options.add(absolutePath); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager javaFileManager = compiler.getStandardFileManager(null, null, null); JavaClassObject sourceObject = new JavaClassObject("B", sb.toString()); Iterable<? extends JavaFileObject> fileObjects = Arrays.asList(sourceObject); JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, null, options, null, fileObjects); boolean result = task.call(); if (result) { try { //編譯成功,進行載入執行 Class<?> clazz = DynamicProxy.class.getClassLoader().loadClass(String.format("%s.%s", packageName, className)); Constructor<?> constructor = clazz.getConstructor(new Class[]{IA.class}); Object obj = constructor.newInstance(a); return (IA) obj; } catch (Exception e) { e.printStackTrace(); } } return null; }}class JavaClassObject extends SimpleJavaFileObject { private String content = null; public JavaClassObject(String name, String content) { super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); this.content = content; } public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return content; }}
我們可以這麼呼叫:
public static void main(String[] args) { DynamicProxy dynamicProxy = new DynamicProxy(); IA aProxy = dynamicProxy.createProxyObj(new A()); aProxy.method1("Java"); }
第三種方式實現的動態代理,不推薦使用,我們直接使用第一種或者第二種即可,更加方便和簡潔。
關於實現java動態編譯的方式有很多種,上述方式是使用了jdk自帶的rt.jar中的javax.tools包提供的編譯器實現的。
關於動態編譯,我們之後會拿出一個章節進行專門講解。本章節就一帶而過了。