Java中通过代理对类进行修改
JAVA中的靜態(tài)代理、動態(tài)代理及JDK proxy和CGLIB、Javassist、ASM實踐
簡介
Java中對已經(jīng)有的類進行修改,改變或調(diào)整其執(zhí)行,這可以通過代理來實現(xiàn)。Java的class文件是二進制文件,內(nèi)容是只有JVM虛擬機能夠識別的機器碼。JVM虛擬機讀取字節(jié)碼文件,取出二進制數(shù)據(jù),加載到內(nèi)存中,解析.class 文件內(nèi)的信息,生成對應(yīng)的 Class對象。如果對class文件進行了修改,就可以改變程序。
先給一個簡單的示例,演示加載class的二進制文件,得到class的實例對象,然后調(diào)用。
package samples1; // 定義一個簡單的類public class SimpleCode { public void code() { System.out.println("--SimpleCode's code"); } } package samples1; /** * 自定義一個類加載器,用于將字節(jié)碼轉(zhuǎn)換為class對象 */ public class SimpleClassLoader extends ClassLoader { public Class<?> defineMyClass( byte[] b, int off, int len) { //return super.defineClass(b, off, len); return super.defineClass(null, b, off, len,null);} }//
package samples1; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; public class MyTest { public static void main(String[] args) throws IOException { //讀取本地的class文件內(nèi)的字節(jié)碼,轉(zhuǎn)換成字節(jié)碼數(shù)組 File file = new File("."); InputStream input = new FileInputStream(file.getCanonicalPath()+"/classes/samples1/SimpleCode.class");byte[] result = new byte[1024];int count = input.read(result);// 使用自定義的類加載器將 byte字節(jié)碼數(shù)組轉(zhuǎn)換為對應(yīng)的class對象 SimpleClassLoader loader = new SimpleClassLoader(); Class clazz = loader.defineMyClass( result, 0, count); //測試加載是否成功,打印class 對象的名稱 System.out.println(clazz.getCanonicalName()); //實例化一個Programmer對象 try { Object o= clazz.newInstance(); //調(diào)用SimpleCode的code方法 clazz.getMethod("code", null).invoke(o, null); } catch (IllegalArgumentException | NoSuchMethodException | SecurityException e) { e.printStackTrace(); } catch (Exception e) {e.printStackTrace();}} }編譯執(zhí)行:
$ javac -cp classes -d classes *.java $ $ java -cp classes samples1.MyTest class Name: samples1.SimpleCode --SimpleCode's code $從上面的示例中我們可以看到,通過class文件,我們可以自己加載并生成一個類實例,并進行調(diào)用。
如果我們在運行期系統(tǒng)中,遵循Java編譯系統(tǒng)組織.class文件的格式和結(jié)構(gòu),生成相應(yīng)的二進制數(shù)據(jù),然后再把這個二進制數(shù)據(jù)加載轉(zhuǎn)換成對應(yīng)的類,這樣,就完成了在代碼中,動態(tài)創(chuàng)建一個類的能力了。
JAVA中的靜態(tài)代理、動態(tài)代理
- 靜態(tài)代理類:由程序員創(chuàng)建或由特定工具自動生成源代碼,再對其編譯。在程序運行前,代理類的.class文件就已經(jīng)存在了。
- 動態(tài)代理類:在程序運行時,運用反射機制動態(tài)創(chuàng)建而成。
靜態(tài)代理通常只代理一個類,動態(tài)代理是代理一個接口下的多個實現(xiàn)類。靜態(tài)代理事先知道要代理的是什么,而動態(tài)代理不知道要代理什么東西,只有在運行時才知道。
動態(tài)代理是實現(xiàn)JDK里的InvocationHandler接口的invoke方法,但注意的是代理的是接口,也就是你的業(yè)務(wù)類必須要實現(xiàn)接口,通過Proxy里的newProxyInstance得到代理對象。
實現(xiàn)動態(tài)代理有幾個第三方的庫,提供了方便的代理實現(xiàn)。
還有一種動態(tài)代理CGLIB,代理的是類,不需要業(yè)務(wù)類繼承接口,通過派生的子類來實現(xiàn)代理。通過在運行時,動態(tài)修改字節(jié)碼達到修改類的目的。
Java靜態(tài)代理示例
定義了一個HelloService接口,有2個實現(xiàn),HelloServiceProxy、HelloServiceImpl,這兩個類都實現(xiàn)了HelloService接口。其中HelloServiceImpl類是HelloService接口的真正實現(xiàn)者,是一個委托類,而HelloServiceProxy類是通過調(diào)用HelloServiceImpl類的相關(guān)方法來提供特定服務(wù)的,是一個代理類。
HelloServiceProxy類的echo()方法和getTime()方法會分別調(diào)用被代理的HelloServiceImpl對象的echo()方法和getTime()方法,并且在方法調(diào)用前后都會執(zhí)行一些操作,比如簡單的記錄一些信息。由此可見,代理類可以為委托類預(yù)處理消息、把消息轉(zhuǎn)發(fā)給委托類和事后處理消息等。
package proxy; import java.util.Date; public interface HelloService{ public String echo(String msg); public Date getTime(); } package proxy; import java.util.Date; public class HelloServiceImpl implements HelloService{ public String greet(String name){ return "Hi, "+name; } public Date getTime(){ return new Date(); } } package proxy; import java.util.Date; public class HelloServiceProxy implements HelloService{ //表示被代理的HelloService 實例 private HelloService helloService; public HelloServiceProxy(HelloService helloService){ this.helloService=helloService; } public void setHelloServiceProxy(HelloService helloService){ this.helloService=helloService; } public String greet(String name){ //預(yù)處理 System.out.println("before calling greet()"); //調(diào)用被代理的HelloService 實例的greet()方法 String result=helloService.greet(name); //事后處理 System.out.println("after calling greet()"); return result; } public Date getTime(){ //預(yù)處理 System.out.println("before calling getTime()"); //調(diào)用被代理的HelloService 實例的getTime()方法 Date date=helloService.getTime(); //事后處理 System.out.println("after calling getTime()"); return date; } }使用的測試類
package proxy; public class ProxyTest{ public static void main(String args[]){ HelloService helloService=new HelloServiceImpl(); HelloService helloServiceProxy=new HelloServiceProxy(helloService); System.out.println(helloServiceProxy.greet("David")); System.out.println(helloServiceProxy.getTime());} }編譯并執(zhí)行,查看結(jié)果。
$ javac -d classes -cp classes *.java $ java -cp classes proxy.ProxyTest before calling greet() after calling greet() Hi, David before calling getTime() after calling getTime() Mon Oct 16 15:47:09 CST 2017 $總結(jié): 靜態(tài)代理要使用的class在使用前都存在,在代理類中知道并可以使用,這種代理類就是靜態(tài)代理類。
Java 動態(tài)代理
和靜態(tài)代理類不同,動態(tài)代理類的字節(jié)碼在程序運行時由Java反射機制動態(tài)生成,無需程序員手工編寫它的源代碼。動態(tài)代理類不僅簡化了編程工作,而且提高了軟件系統(tǒng)的可擴展性,因為Java反射機制可以生成任意類型的動態(tài)代理類。
java.lang.reflect 包中的Proxy類和InvocationHandler接口提供了生成動態(tài)代理類的能力。
Proxy類提供了創(chuàng)建動態(tài)代理類及其實例的靜態(tài)方法。包括:
(1)getProxyClass()靜態(tài)方法負責創(chuàng)建動態(tài)代理類,它的完整定義如下:
public static Class
InvocationHandler handler = new MyInvocationHandler(...);Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).newInstance(handler);或者使用簡化的方法:
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class<?>[] { Foo.class },handler);JavaDoc中介紹了一個代理類有一些特點:
由Proxy類的靜態(tài)方法創(chuàng)建的動態(tài)代理類具有以下特點:
- 動態(tài)代理類是public、final和非抽象類型,如果其代理的接口都是public的;如果有接口是non-public, 則代理類也是non-public.
- 動態(tài)代理類繼承了java.lang.reflect.Proxy類;
- 動態(tài)代理類的名字以“$Proxy”開頭;
- … …
由Proxy類的靜態(tài)方法創(chuàng)建的動態(tài)代理類的實例具有以下特點:
1. 假定變量foo 是一個動態(tài)代理類的實例,并且這個動態(tài)代理類實現(xiàn)了Foo 接口,那么foo instanceof Foo的值為true。把變量foo強制轉(zhuǎn)換為Foo類型是合法的:(Foo) foo //合法。
每個動態(tài)代理類實例都和一個`InvocationHandler 實例關(guān)聯(lián)。Proxy類的getInvocationHandler(Objectproxy)靜態(tài)方法返回與參數(shù)proxy指定的代理類實例所關(guān)聯(lián)的InvocationHandler 對象。
假定Foo接口有一個amethod()方法,那么當程序調(diào)用動態(tài)代理類實例foo的amethod()方法時,該方法會調(diào)用與它關(guān)聯(lián)的InvocationHandler對象的invoke()方法。
InvocationHandler 接口為方法調(diào)用接口,它聲明了負責調(diào)用任意一個方法的invoke()方法.
Object invoke(Object proxy,Method method,Object[] args) throwsThrowable參數(shù)proxy指定動態(tài)代理類實例,參數(shù)method指定被調(diào)用的方法,參數(shù)args指定向被調(diào)用方法傳遞的參數(shù),invoke()方法的返回值表示被調(diào)用方法的返回值。下面看一個示例:
package DynamicProxy; /** * 抽象接口*/ public interface Subject { public void greet(); } package DynamicProxy; public class RealSubject implements Subject{ @Override public void greet() { //System.out.println("greet By---"+getClass()); } }最重要的是:建立InvocationHandler用來響應(yīng)代理的任何調(diào)用。
package DynamicProxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class ProxyHandler implements InvocationHandler { private Object proxied; public ProxyHandler( Object proxied ) { this.proxied = proxied; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("prepare, at proxy!"); //轉(zhuǎn)調(diào)具體目標對象的方法 Object object= method.invoke(proxied, args); System.out.println("finished, at proxy!"); return object; } }測試類:
package DynamicProxy; import java.lang.reflect.Proxy; public class DynamicProxyTest { public static void main( String args[] ) { RealSubject realImpl = new RealSubject(); Subject proxySubject = (Subject)Proxy.newProxyInstance( Subject.class.getClassLoader(),new Class[]{Subject.class}, new ProxyHandler(realImpl) ); proxySubject.greet();; } }執(zhí)行,檢查結(jié)果:
$ mkdir classes $ javac -d classes *.java $ java -cp classes DynamicProxy.DynamicProxyTestprepare, at proxy! greet By---class DynamicProxy.RealSubject finished, at proxy!$查看JDK中proxy的source code:
public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces)throws IllegalArgumentException {final Class<?>[] intfs = interfaces.clone();final SecurityManager sm = System.getSecurityManager();if (sm != null) {checkProxyAccess(Reflection.getCallerClass(), loader, intfs);}return getProxyClass0(loader, intfs); }private static void checkProxyAccess(Class<?> caller,ClassLoader loader,Class<?>... interfaces) {SecurityManager sm = System.getSecurityManager();if (sm != null) {ClassLoader ccl = caller.getClassLoader();if (VM.isSystemDomainLoader(loader) && !VM.isSystemDomainLoader(ccl)) {sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);}ReflectUtil.checkProxyPackageAccess(ccl, interfaces);} }/*** Generate a proxy class. Must call the checkProxyAccess method* to perform permission checks before calling this.*/ private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {if (interfaces.length > 65535) {throw new IllegalArgumentException("interface limit exceeded");}// If the proxy class defined by the given loader implementing// the given interfaces exists, this will simply return the cached copy;// otherwise, it will create the proxy class via the ProxyClassFactoryreturn proxyClassCache.get(loader, interfaces); } public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)throws IllegalArgumentException{Objects.requireNonNull(h);final Class<?>[] intfs = interfaces.clone();final SecurityManager sm = System.getSecurityManager();if (sm != null) {checkProxyAccess(Reflection.getCallerClass(), loader, intfs);}/** Look up or generate the designated proxy class.*/Class<?> cl = getProxyClass0(loader, intfs);/** Invoke its constructor with the designated invocation handler.*/try {if (sm != null) {checkNewProxyPermission(Reflection.getCallerClass(), cl);}final Constructor<?> cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;if (!Modifier.isPublic(cl.getModifiers())) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {cons.setAccessible(true);return null;}});}return cons.newInstance(new Object[]{h});} catch (IllegalAccessException|InstantiationException e) {throw new InternalError(e.toString(), e);} catch (InvocationTargetException e) {Throwable t = e.getCause();if (t instanceof RuntimeException) {throw (RuntimeException) t;} else {throw new InternalError(t.toString(), t);}} catch (NoSuchMethodException e) {throw new InternalError(e.toString(), e);}}在測試類main方法中,添加System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");,將會把生成的代理類保留在磁盤上,文件如此:com/sun/proxy/$Proxy0.class,可以反編譯查看。
總結(jié)
動態(tài)代理使用InvocationHandler的作用:
在靜態(tài)代理中,代理Proxy中的方法,都指定了調(diào)用了特定的realSubject中的對應(yīng)的方法:在靜態(tài)代理模式下,Proxy所做的事情,就是調(diào)用Subject接口的方法時,調(diào)用realSubject對應(yīng)的方法;動態(tài)代理工作的基本模式就是將自己的方法功能的實現(xiàn)交給 InvocationHandler角色,外界對Proxy角色中的每一個方法的調(diào)用,Proxy角色都會交給InvocationHandler來處理,而InvocationHandler則調(diào)用具體對象角色的方法。不同的代理實現(xiàn)庫
代理Proxy和RealSubject應(yīng)該實現(xiàn)相同的API,在面向?qū)ο蟮木幊讨?#xff0c;如果我們想要約定Proxy 和RealSubject可以實現(xiàn)相同的功能,有兩種方式:
- a. 一個比較直觀的方式,就是定義一個功能接口,然后讓Proxy 和RealSubject來實現(xiàn)這個接口。JDK使用此思路實現(xiàn)代理。
b. 還有比較隱晦的方式,就是通過繼承。因為如果Proxy 繼承自RealSubject,這樣Proxy則擁有了RealSubject的功能,Proxy還可以通過重寫RealSubject中的方法,來實現(xiàn)多態(tài)。而cglib 則是以此思路設(shè)計的。
cglib代理實現(xiàn)
JDK中提供的生成動態(tài)代理類的機制有個鮮明的特點是: 某個類必須有實現(xiàn)的接口,而生成的代理類也只能代理某個類接口定義的方法。極端的情況是:如果某個類沒有實現(xiàn)接口,那么這個類就不能同JDK產(chǎn)生動態(tài)代理了!
而cglib–“CGLIB(Code Generation Library),是一個強大的,高性能,高質(zhì)量的Code生成類庫,它可以在運行期擴展Java類與實現(xiàn)Java接口。”
cglib 創(chuàng)建某個類A的動態(tài)代理類的模式是:
- 1. 查找A上的所有非final 的public類型的方法定義;
- 2. 將這些方法的定義轉(zhuǎn)換成字節(jié)碼;
- 3. 將組成的字節(jié)碼轉(zhuǎn)換成相應(yīng)的代理的class對象;
- 4. 實現(xiàn) MethodInterceptor接口,用來處理 對代理類上所有方法的請求(這個接口和JDK動態(tài)代理InvocationHandler的功能和角色是一樣的)
網(wǎng)友有個有趣的例子:定義一個Programmer類,一個Hacker類
package samples; public class Programmer { public void code() { System.out.println("I'm a Programmer,Just Coding....."); } } package samples; import java.lang.reflect.Method; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; /* * 實現(xiàn)了方法攔截器接口 */ public class Hacker implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("**** I am a hacker,Let's see what the poor programmer is doing Now..."); proxy.invokeSuper(obj, args); System.out.println("**** Oh,what a poor programmer....."); return null; } }package samples;import net.sf.cglib.proxy.Enhancer;public class Test {public static void main(String[] args) {Programmer progammer = new Programmer();Hacker hacker = new Hacker(); //cglib 中加強器,用來創(chuàng)建動態(tài)代理 Enhancer enhancer = new Enhancer();//設(shè)置要創(chuàng)建動態(tài)代理的類 enhancer.setSuperclass(progammer.getClass()); // 設(shè)置回調(diào),這里相當于是對于代理類上所有方法的調(diào)用,都會調(diào)用CallBack,// 而Callback則需要實行intercept()方法進行攔截 enhancer.setCallback(hacker); Programmer proxy =(Programmer)enhancer.create(); proxy.code();} }
執(zhí)行
**** I am a hacker,Let's see what the poor programmer is doing Now... I'm a Programmer,Just Coding..... **** Oh,what a poor programmer.....在cglib中一些關(guān)鍵的類的列表:
- java.lang.reflect.Method;
- net.sf.cglib.core.ReflectUtils;
- net.sf.cglib.core.Signature;
- net.sf.cglib.proxy.Callback;
- net.sf.cglib.proxy.Factory;
- net.sf.cglib.proxy.MethodInterceptor;
- net.sf.cglib.proxy.MethodProxy;
Javassist–Java字節(jié)碼的操作類庫
Javassist是一個開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫。是由東京工業(yè)大學的數(shù)學和計算機科學系的 Shigeru Chiba所創(chuàng)建的。
它已加入了開放源代碼JBoss 應(yīng)用服務(wù)器項目,通過使用Javassist對字節(jié)碼操作為JBoss實現(xiàn)動態(tài)AOP框架。javassist是jboss的一個子項目,其主要的優(yōu)點,在于簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態(tài)改變類的結(jié)構(gòu),或者動態(tài)生成類。
下面展示一個簡單的使用示例:
import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.CtNewMethod; public class MyGenerator { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); //創(chuàng)建Programmer類 CtClass cc= pool.makeClass("com.samples.Programmer"); //定義code方法 CtMethod method = CtNewMethod.make("public void code(){}", cc); //插入方法代碼 method.insertBefore("System.out.println(\"I'm a Programmer,Just Coding.....\");"); cc.addMethod(method); //保存生成的字節(jié)碼 cc.writeFile("d://temp"); } }Java字節(jié)碼生成開源框架–ASM
ASM 是一個 Java 字節(jié)碼操控框架。它能夠以二進制形式修改已有類或者動態(tài)生成類。ASM 可以直接產(chǎn)生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態(tài)改變類行為。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。不過ASM在創(chuàng)建class字節(jié)碼的過程中,操縱的級別是底層JVM的匯編指令級別,這要求ASM使用者要對class組織結(jié)構(gòu)和JVM匯編指令有一定的了解。比如,有下面的programmer類: package com.samples; import java.io.PrintStream; public class Programmer { public void code() { System.out.println("I'm a Programmer,Just Coding....."); } }當沒有此Programmer類源碼,而使用ASM來生成,可以這么實現(xiàn).
使用ASM框架提供了ClassWriter 接口,通過訪問者模式進行動態(tài)創(chuàng)建class字節(jié)碼,
package samples; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class MyGenerator { public static void main(String[] args) throws IOException { System.out.println(); ClassWriter classWriter = new ClassWriter(0); // 通過visit方法確定類的頭部信息 classWriter.visit(Opcodes.V1_7,// java版本 Opcodes.ACC_PUBLIC,// 類修飾符 "Programmer", // 類的全限定名 null, "java/lang/Object", null); //創(chuàng)建構(gòu)造函數(shù) MethodVisitor mv = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>","()V"); mv.visitInsn(Opcodes.RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // 定義code方法 MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "code", "()V", null, null); methodVisitor.visitCode(); methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); methodVisitor.visitLdcInsn("I'm a Programmer,Just Coding....."); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"); methodVisitor.visitInsn(Opcodes.RETURN); methodVisitor.visitMaxs(2, 2); methodVisitor.visitEnd(); classWriter.visitEnd(); // 使classWriter類已經(jīng)完成 // 將classWriter轉(zhuǎn)換成字節(jié)數(shù)組寫到文件里面去 byte[] data = classWriter.toByteArray(); File file = new File("Programmer.class"); FileOutputStream fout = new FileOutputStream(file); fout.write(data); fout.close(); } }總結(jié)
以上是生活随笔為你收集整理的Java中通过代理对类进行修改的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 美在线支付巨头PayPal计划裁员200
- 下一篇: Java读取文件时第一行出现乱码“?”问