Java中通过代理对类进行修改

JAVA中的静态代理、动态代理及JDK proxy和CGLIB、Javassist、ASM实践

简介

Java中对已经有的类进行修改,改变或调整其执行,这可以通过代理来实现。Java的class文件是二进制文件,内容是只有JVM虚拟机能够识别的机器码。JVM虚拟机读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息,生成对应的 Class对象。如果对class文件进行了修改,就可以改变程序。

先给一个简单的示例,演示加载class的二进制文件,得到class的实例对象,然后调用。

package samples1; 

// 定义一个简单的类
 public class SimpleCode {  

    public void code()  
    {  
        System.out.println("--SimpleCode's code");  
    }  
}  
package samples1; 
 /** 
  * 自定义一个类加载器,用于将字节码转换为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文件内的字节码,转换成字节码数组  
        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字节码数组转换为对应的class对象  
        SimpleClassLoader loader = new SimpleClassLoader();  
        Class clazz = loader.defineMyClass( result, 0, count);  

        //测试加载是否成功,打印class 对象的名称  
        System.out.println(clazz.getCanonicalName());  

        //实例化一个Programmer对象  
        try 
        {  
        Object o= clazz.newInstance();  
            //调用SimpleCode的code方法  
            clazz.getMethod("code", null).invoke(o, null);  
        } 
        catch (IllegalArgumentException  
                | NoSuchMethodException | SecurityException e) 
        {  
                e.printStackTrace();  
        }  
        catch (Exception e) {
            e.printStackTrace();
        }
    }  
}

编译执行:

$ javac -cp classes -d classes *.java
$
$ java -cp classes  samples1.MyTest
class Name: samples1.SimpleCode
--SimpleCode's code
$ 

从上面的示例中我们可以看到,通过class文件,我们可以自己加载并生成一个类实例,并进行调用。
如果我们在运行期系统中,遵循Java编译系统组织.class文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,这样,就完成了在代码中,动态创建一个类的能力了。

JAVA中的静态代理、动态代理

  • 静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
  • 动态代理类:在程序运行时,运用反射机制动态创建而成。

静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。

动态代理是实现JDK里的InvocationHandler接口的invoke方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过Proxy里的newProxyInstance得到代理对象。

实现动态代理有几个第三方的库,提供了方便的代理实现。

还有一种动态代理CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。

Java静态代理示例

定义了一个HelloService接口,有2个实现,HelloServiceProxy、HelloServiceImpl,这两个类都实现了HelloService接口。其中HelloServiceImpl类是HelloService接口的真正实现者,是一个委托类,而HelloServiceProxy类是通过调用HelloServiceImpl类的相关方法来提供特定服务的,是一个代理类。

HelloServiceProxy类的echo()方法和getTime()方法会分别调用被代理的HelloServiceImpl对象的echo()方法和getTime()方法,并且在方法调用前后都会执行一些操作,比如简单的记录一些信息。由此可见,代理类可以为委托类预处理消息、把消息转发给委托类和事后处理消息等。

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){  
    //预处理  
    System.out.println("before calling greet()");   
     //调用被代理的HelloService 实例的greet()方法  
    String result=helloService.greet(name);  
    //事后处理  
    System.out.println("after calling greet()");   
    return result;  
  }  

  public Date getTime(){  
    //预处理  
    System.out.println("before calling getTime()");   
     //调用被代理的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());
  }  

}  

编译并执行,查看结果。

$ 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
$ 

总结: 静态代理要使用的class在使用前都存在,在代理类中知道并可以使用,这种代理类就是静态代理类。

Java 动态代理

和静态代理类不同,动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java反射机制可以生成任意类型的动态代理类。

java.lang.reflect 包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。

Proxy类提供了创建动态代理类及其实例的静态方法。包括:

(1)getProxyClass()静态方法负责创建动态代理类,它的完整定义如下:

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类的静态方法创建的动态代理类具有以下特点:
- 动态代理类是public、final和非抽象类型,如果其代理的接口都是public的;如果有接口是non-public, 则代理类也是non-public.
- 动态代理类继承了java.lang.reflect.Proxy类;
- 动态代理类的名字以“$Proxy”开头;
- … …

由Proxy类的静态方法创建的动态代理类的实例具有以下特点:
1. 假定变量foo 是一个动态代理类的实例,并且这个动态代理类实现了Foo 接口,那么foo instanceof Foo的值为true。把变量foo强制转换为Foo类型是合法的:(Foo) foo //合法

  1. 每个动态代理类实例都和一个`InvocationHandler 实例关联。Proxy类的getInvocationHandler(Objectproxy)静态方法返回与参数proxy指定的代理类实例所关联的InvocationHandler 对象。

  2. 假定Foo接口有一个amethod()方法,那么当程序调用动态代理类实例foo的amethod()方法时,该方法会调用与它关联的InvocationHandler对象的invoke()方法。

InvocationHandler 接口为方法调用接口,它声明了负责调用任意一个方法的invoke()方法.

Object invoke(Object proxy,Method method,Object[] args) throwsThrowable

参数proxy指定动态代理类实例,参数method指定被调用的方法,参数args指定向被调用方法传递的参数,invoke()方法的返回值表示被调用方法的返回值。

下面看一个示例:

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用来响应代理的任何调用。

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!");  

        //转调具体目标对象的方法  
          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();;  

    }     
}  

执行,检查结果:

$ mkdir classes
$ javac -d classes *.java
$ java -cp classes DynamicProxy.DynamicProxyTest

prepare, 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 ProxyClassFactory
    return 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,可以反编译查看。

总结

动态代理使用InvocationHandler的作用:

在静态代理中,代理Proxy中的方法,都指定了调用了特定的realSubject中的对应的方法:

在静态代理模式下,Proxy所做的事情,就是调用Subject接口的方法时,调用realSubject对应的方法;

动态代理工作的基本模式就是将自己的方法功能的实现交给 InvocationHandler角色,外界对Proxy角色中的每一个方法的调用,Proxy角色都会交给InvocationHandler来处理,而InvocationHandler则调用具体对象角色的方法。

不同的代理实现库

代理Proxy和RealSubject应该实现相同的API,在面向对象的编程之中,如果我们想要约定Proxy 和RealSubject可以实现相同的功能,有两种方式:
- a. 一个比较直观的方式,就是定义一个功能接口,然后让Proxy 和RealSubject来实现这个接口。JDK使用此思路实现代理。

  • b. 还有比较隐晦的方式,就是通过继承。因为如果Proxy 继承自RealSubject,这样Proxy则拥有了RealSubject的功能,Proxy还可以通过重写RealSubject中的方法,来实现多态。而cglib 则是以此思路设计的。

    cglib代理实现

    JDK中提供的生成动态代理类的机制有个鲜明的特点是: 某个类必须有实现的接口,而生成的代理类也只能代理某个类接口定义的方法。极端的情况是:如果某个类没有实现接口,那么这个类就不能同JDK产生动态代理了!

    而cglib–“CGLIB(Code Generation Library),是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。”

cglib 创建某个类A的动态代理类的模式是:
- 1. 查找A上的所有非final 的public类型的方法定义;
- 2. 将这些方法的定义转换成字节码;
- 3. 将组成的字节码转换成相应的代理的class对象;
- 4. 实现 MethodInterceptor接口,用来处理 对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)

网友有个有趣的例子:定义一个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;  

/* 
    * 实现了方法拦截器接口 
    */  
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 中加强器,用来创建动态代理  
        Enhancer enhancer = new Enhancer();

        //设置要创建动态代理的类  
        enhancer.setSuperclass(progammer.getClass());   

        // 设置回调,这里相当于是对于代理类上所有方法的调用,都会调用CallBack,
        // 而Callback则需要实行intercept()方法进行拦截  
        enhancer.setCallback(hacker);  
        Programmer proxy =(Programmer)enhancer.create();  
        proxy.code();

    }
}

执行

**** 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中一些关键的类的列表:
- 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字节码的操作类库

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba所创建的。

它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

下面展示一个简单的使用示例:

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();  

        //创建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);  

        //保存生成的字节码  
        cc.writeFile("d://temp");  
    }  
}  

Java字节码生成开源框架–ASM

ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和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来生成,可以这么实现.

使用ASM框架提供了ClassWriter 接口,通过访问者模式进行动态创建class字节码,

 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);  

        //创建构造函数  
        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类已经完成  

        // 将classWriter转换成字节数组写到文件里面去  
        byte[] data = classWriter.toByteArray();  
        File file = new File("Programmer.class");  
        FileOutputStream fout = new FileOutputStream(file);  
        fout.write(data);  
        fout.close();  
    }  
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值