Spring中的代理

Author Avatar
Rico 9月 01, 2018

代理模式(Proxy)

特点:

1、 有2个角色,执行者、被代理人

2、 对于被代理人来说,这件事情是一定要做的,但是我自己不想做或者没有时间做,找代理做

3、 需要获取到被代理的人个人资料,只是参与整个过程的某个或几个环节

生活中的案例:

租房中介、

售票黄牛、

婚介、

经纪人

快递、事务代理、非侵入式日志监听

应用场景:为其他对象提供一种代理以控制对这个对象的访问。从结构上来看和Decorator模式类似,但Proxy是控制,更像是一种对功能的限制,而Decorator是增加职责。

Spring的Proxy模式在AOP中有体现,比如JdkDynamicAopProxy和Cglib2AopProxy。

代码举例:租房子

定义一个人,要找房子

package findHouse;
public interface Person {
    String getName();
    String getAddress();
    void findHouse();
    void anotherMethod();
}

定义个张三,要找房子

package findHouse;
public class Zhangsan implements Person{
    private String name="张三";
    private String address="西湖";
    public void findHouse() {
        System.out.println("我是"+this.getName()+",我住"+this.getAddress()+"附近,要租房子");
    }
    public void anotherMethod() { System.out.println("另一个方法"); }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getAddress() { return address; }
    public void setAddress(String address) { this.address = address; }
}

如果不使用任何模式,最简单的调用是这样

package findHouse;
public class TestFindHouse {
    public static void main(String[] args) {
        Person person=new Zhangsan();
        person.findHouse();
    }
}

但是张三是个上班族,没时间租房子,就去找中介。

中介类

在使用动态代理时,我们需要定义一个位于代理类与委托类之间的中介类,这个中介类被要求实现InvocationHandler接口

package findHouse;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//中介
public class Zhongjie implements InvocationHandler{//实现这个接口才具有代理功能
    private Person target;
    //获取被代理人的个人资料
    public Object getInstance(Person target) throws Exception {
        this.target=target;
        Class clazz=target.getClass();
        ClassLoader loader=clazz.getClassLoader();
        return Proxy.newProxyInstance(loader, clazz.getInterfaces(), this);//第一个参数是ClassLoader,第二个参数是interfaces,第三个参数是InvocationHandler
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理开始");
        System.out.println("我是中介,你叫"+this.target.getName()+",你住"+this.target.getAddress()+"附近,我帮你找房子,找了几个合适的你挑选一下");
        //this.target.findHouse();
        Object result = method.invoke(this.target, args);//这里其实并没有返回值
        System.out.println("代理结束");
        return result;
    }
}

从InvocationHandler这个名称我们就可以知道,实现了这个接口的中介类用做“调用处理器”。当我们调用代理类对象的方法时,这个“调用”会转送到invoke方法中,代理类对象作为proxy参数传入,参数method标识了我们具体调用的是代理类的哪个方法,args为这个方法的参数。这样一来,我们对代理类中的所有方法的调用都会变为对invoke的调用,这样我们可以在invoke方法中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)

中介类实现InvocationHandler接口,作为调用处理器”拦截“对代理类方法的调用

测试类

package findHouse;
public class TestFindHouse {
    public static void main(String[] args) {
        Zhongjie zhongjie=new Zhongjie();//中介
        try {
            Person obj=(Person)zhongjie.getInstance(new Zhangsan());
            System.out.println("代理后生成的class:"+obj.getClass().getName());
            obj.findHouse();
            //obj.anotherMethod();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

从以上代码中我们可以看到,中介类持有一个委托类对象引用,在invoke方法中调用了委托类对象的相应方法

感觉上就是 ,中介类可以在调用张三的找房方法前,先执行一些逻辑,然后调用张三的找房,然后在执行以下后续方法。

有点像我们现实生活中的情况,比如你要买房,中介在你签字前做了大量工作,找房东,拿钥匙,联系贷款银行问利率,帮你算总价,分期等,你决定要买了之后你只需要签字即可(调用委托对象的相应方法),你签完字,中介还要继续后续银行放款,权证房产证契税缴纳,房管局取送资料,最后交付。 你只需要签字付钱即可。

贷款利率变了,手续流程变了,你不需要关心,你只要签字,脏活累活业务强相关的代理类帮你干了。

在编程中也一样,某些东西流程变了,委托类不需要管,代理类帮你管好,你的核心流程不变。这样就省心多了

动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数

原理:

1 拿到被代理对象的引用(张三类),然后获取它的接口

2 JDK代理重新生成一个类,同时实现我们给的代理对象所实现的接口 (Person接口)

3 把被代理对象的引用也拿到了

4 重新动态生成一个class字节码

5 然后编译

你委托类(被代理类)的原始方法是不改的,只是在你前面,在你后面加了一些逻辑

把JVM编译后的代理类的class字节获取到然后反编译看下

这个this.h是什么? 在父类Proxy中可以看到这个h

this.h就是InvocatinHandler

我们把生成的proxy0.class的字节码反编译输出看下

h就是InvocationHandler

我们代码中

Proxy.newProxyInstance(loader, clazz.getInterfaces(), this) 把Zhongjie的this当做第三个参数传入,所以h就是中介类

所以上图代码中的

this.h.invoke(this.m4.null)就是调用了 zhangsan类中的invoke方法,而中介类的invoke的代码是

System.out.println("代理开始");
System.out.println("我是中介,你叫"+this.target.getName()+",你住"+this.target.getAddress()+"附近,我帮你找房子,找了几个合适的你挑选一下");
Object result = method.invoke(this.target, args);//这里其实并没有返回值
System.out.println("代理结束");
return result;

其中有一行是

Object result = method.invoke(this.target, args);

又调用了method.lnvoke,这才真正调用了张三的实际方法,但是在调用张三方法之前和之后,就是我们在代理类中需要在调用张三方法之前需要执行的操作,达到了我们的目的

了解清楚代理原理和代理模式对后续看Spring源码有帮助。 修炼内功心法

自己编写Proxy和ClassLoader,InvocationHandler

根据接口生成java代码的方法

     //重新根据接口生成代码(拼代码)
     private static  String generateSourceCode(Class<?> interfaces) {
         //包名
         StringBuilder sourceCode=new StringBuilder("package org.rico.learnSpring.customProxy;").append(ln);//包名
         //导包
         sourceCode.append("import java.lang.reflect.Method;").append(ln).append(ln);
         //声明类
         sourceCode.append("public  class $Proxy0 implements ").append(interfaces.getName()).append("{").append(ln);//这里只实现了一个,如果通用的话要写循环//这里的类名是随便的,只要.java文件和类名一样就可以了这里是$Proxy0
         sourceCode.append("  MyInvocationHandler h;").append(ln);//域 局部变量
         //构造函数
         sourceCode.append("  public $Proxy0(MyInvocationHandler h){").append(ln);//这个构造函数名应该是动态生成的,但是这里写死了,为了方便
         sourceCode.append("    this.h=h;").append(ln);
         sourceCode.append("}").append(ln);//匹配public $Proxy0的{
         for(Method m:interfaces.getMethods()){
             sourceCode.append("  public ").append(m.getReturnType().getName()).append(" ").append(m.getName()).append("(){").append(ln);//我们的Person方法都是无参的,这里也是图简便,应该是要把参数也遍历出来的
             sourceCode.append("    try{").append(ln);
             //获取这个Method
             sourceCode.append("        Method m = ").append(interfaces.getName()).append(".class.getMethod(\"").append(m.getName()).append("\",new Class[0]);").append(ln);//new Class[]{}
             sourceCode.append("        this.h.invoke(this,m,null);").append(ln);
             sourceCode.append("    }catch(Throwable e){e.printStackTrace();}").append(ln);
             if(!m.getReturnType().getName().equals("void")){//如果返回值不为空,返回
                 sourceCode.append("    return null;").append(ln);//这里为了方便直接返回null
             }
             sourceCode.append("  }").append(ln);
         }
         sourceCode.append("}");//匹配public class 的 {
         return sourceCode.toString();
     }

生成的java代码

编写的这个generageSourceCode方法也是不断尝试,直到生成的.java文件放在工程中不报错了就可以了 ,经常会出现括号不成对出现,返回值,大小写等的问题

编译java文件成class文件

        //3 编译源代码 ,并且生成.class文件 
         try {
             JavaCompiler compiler= ToolProvider.getSystemJavaCompiler();//javax提供的编译器
             StandardJavaFileManager manager=compiler.getStandardFileManager(null,null,null);
             Iterable iterable=manager.getJavaFileObjects(f);//获取迭代器 就是为了找到Java文件
             JavaCompiler.CompilationTask task=compiler.getTask(null,manager,null,null,null,iterable);//编译任务
             task.call();
             manager.close();
         } catch (IOException e) {
             e.printStackTrace();
         }

完整源码

测试类

package org.rico.learnSpring.customProxy;
import org.rico.learnSpring.findHouse.Person;
import org.rico.learnSpring.findHouse.Zhangsan;
public class TestFindHouse {
    public static void main(String[] args) {
        MyZhongjie zhongjie=new MyZhongjie();//中介
        try {
            Person obj=(Person)zhongjie.getInstance(new Zhangsan());//代理出来的对象可以强转成接口的 ,当然zhangsan也要实现Person
            System.out.println("代理后生成的class:"+obj.getClass().getName());
            obj.findHouse();
            //obj.anotherMethod();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

中介类 MyZhongjie.java

package org.rico.learnSpring.customProxy;
import org.rico.learnSpring.findHouse.Person;
import java.lang.reflect.Method;
//房屋中介类
public class MyZhongjie implements MyInvocationHandler{
    private Person target;
    //获取被代理人的个人资料
    public Object getInstance(Person target) throws Exception {
        this.target=target;
        Class clazz=target.getClass();
        System.out.println("被代理对象的class是:"+clazz.getName());        
        return MyProxy.newProxyInstance(new MyClassLoader(), clazz.getInterfaces(), this);//第一个参数是ClassLoader,第二个参数是interfaces,第三个参数是InvocationHandler也即本身,因为zhongjie本身实现了InvocationHandler
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理开始");
        System.out.println("我是中介,你叫"+this.target.getName()+",你住"+this.target.getAddress()+"附近,我帮你找房子,找了几个合适的你挑选一下");
        Object result = method.invoke(this.target, args);//这里其实并没有返回值
        System.out.println("代理结束");
        return result;
    }
}

其中用到了MyProxy和MyInvocationhandler接口还有MyClassLoader

代理类MyProxy.java的newProxyInstance会生成新的java源码并编译成class然后classLoader会加载进JVM,gtInstance方法return的就是这个新生成并加载的class 然后用Person接口来接收(在测试类中),然后调用findHouse时就调用了这个新生成的类的findHouse(见上图新“生成的类”) 而这个findHouse方法又调用了中介的invoke方法,当然会把实际调用的方法名也传入,例如findhouse,然后调到了中介的invoke方法,中介的invoke方法是

System.out.println("代理开始");
System.out.println("我是中介,你叫"+this.target.getName()+",你住"+this.target.getAddress()+"附近,我帮你找房子,找了几个合适的你挑选一下");
Object result = method.invoke(this.target, args);//这里其实并没有返回值
System.out.println("代理结束");
return result;

其中有一行是

Object result = method.invoke(this.target, args);

又调用了method.lnvoke,这才真正调用了张三的实际方法,但是在调用张三方法之前和之后,就是我们在代理类中需要在调用张三方法之前需要执行的操作,达到了我们的目的

下面是MyProxy的代码

package org.rico.learnSpring.customProxy;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
//这个类会生成代理对象的代码
public class MyProxy {
    private static  final String  ln="\n";//换行
     //这个方法很重要 用于生成代理对象的代码
     public static Object newProxyInstance(MyClassLoader loader,  Class<?>[] interfaces,  MyInvocationHandler h) {
         //1 生成源代码
         String proxySourceCode=generateSourceCode(interfaces[0]);
         //2 将生成的源代码输出到磁盘,保存为.java文件
         final String filePath = MyProxy.class.getResource("").getPath();
         //final String filePath = "C:\\Users\\chenhongjie\\Desktop\\";
         File f= new File(filePath+"$Proxy0.java");//.java文件名和类名一致
         try {
             FileWriter fw=new FileWriter(f);
             fw.write(proxySourceCode);
             fw.flush();
             fw.close();
         } catch (IOException e) {
             e.printStackTrace();
         }
         //3 编译源代码 ,并且生成.class文件 
         try {
             JavaCompiler compiler= ToolProvider.getSystemJavaCompiler();//javax提供的编译器
             StandardJavaFileManager manager=compiler.getStandardFileManager(null,null,null);
             Iterable iterable=manager.getJavaFileObjects(f);//获取迭代器 就是为了找到Java文件
             JavaCompiler.CompilationTask task=compiler.getTask(null,manager,null,null,null,iterable);//编译任务
             task.call();
             manager.close();
             //4 通过MyClassLoader将class文件中的内容动态加载到JVM中来
             Class  proxyClass=loader.findClass("$Proxy0");
             Constructor c=proxyClass.getConstructor(MyInvocationHandler.class);//定义构造函数
             f.delete();//获取到构造函数后可以把源文件删掉 JDK是这么做的,当然你也可以不删
             //5 返回被代理后的代理对象
             return c.newInstance(h);
         } catch (Exception e) {
             e.printStackTrace();
         }
         return null;
     }

     //重新根据接口生成代码(拼代码)
     private static  String generateSourceCode(Class<?> interfaces) {
         //包名
         StringBuilder sourceCode=new StringBuilder("package org.rico.learnSpring.customProxy;").append(ln);//包名
         //导包
         sourceCode.append("import java.lang.reflect.Method;").append(ln).append(ln);
         //声明类
         sourceCode.append("public  class $Proxy0 implements ").append(interfaces.getName()).append("{").append(ln);//这里只实现了一个,如果通用的话要写循环//这里的类名是随便的,只要.java文件和类名一样就可以了这里是$Proxy0
         sourceCode.append("  MyInvocationHandler h;").append(ln);//域 局部变量
         //构造函数
         sourceCode.append("  public $Proxy0(MyInvocationHandler h){").append(ln);//这个构造函数名应该是动态生成的,但是这里写死了,为了方便
         sourceCode.append("    this.h=h;").append(ln);
         sourceCode.append("}").append(ln);//匹配public $Proxy0的{
         for(Method m:interfaces.getMethods()){
             sourceCode.append("  public ").append(m.getReturnType().getName()).append(" ").append(m.getName()).append("(){").append(ln);//我们的Person方法都是无参的,这里也是图简便,应该是要把参数也遍历出来的
             sourceCode.append("    try{").append(ln);
             //获取这个Method
             sourceCode.append("        Method m = ").append(interfaces.getName()).append(".class.getMethod(\"").append(m.getName()).append("\",new Class[0]);").append(ln);//new Class[]{}
             sourceCode.append("        this.h.invoke(this,m,null);").append(ln);
             sourceCode.append("    }catch(Throwable e){e.printStackTrace();}").append(ln);
             if(!m.getReturnType().getName().equals("void")){//如果返回值不为空,返回
                 sourceCode.append("    return null;").append(ln);//这里为了方便直接返回null
             }
             sourceCode.append("  }").append(ln);
         }
         sourceCode.append("}");//匹配public class 的 {
         return sourceCode.toString();
     }
}

MyClassLoader.java的代码

package org.rico.learnSpring.customProxy;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
//实现了代码生成,编译,动态重新load到JVM中
public class MyClassLoader extends ClassLoader {
    private final File baseDir;
    public MyClassLoader(){
        final String basePath=MyClassLoader.class.getResource("").getPath();
        this.baseDir=new File(basePath);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //加载到JVM中来
        String className=MyClassLoader.class.getPackage().getName()+"."+name;
        System.out.println(className);
        if(baseDir!=null){
            File classFile=new File(baseDir,name.replace("\\.","/")+".class");//点.替换成斜杠/
            if(classFile.exists()){
                FileInputStream in=null;
                try{
                    in =new FileInputStream(classFile);
                    ByteArrayOutputStream out=new ByteArrayOutputStream();
                    byte [] buff=new byte[1024];
                    int len;
                    while ((len=in.read(buff))!=-1){
                        out.write(buff,0,len);;
                    }
                    out.close();
                    return defineClass(className,out.toByteArray(),0,out.size());
                }catch (Exception e){
                   e.printStackTrace();
                }finally {
                    if(null!=in){
                        try {
                            in.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    classFile.delete();//加载到jvm后delete掉(要在in,out都删掉之后再关) JDK是删掉的,当然你也可以不删掉
                }
            }
        }
        return null;        
    }
}

MyInvocationHandler.java的代码

JDK原生的InvocationHandler也是一个接口,用于代理类来实现的,本例中就是中介

package org.rico.learnSpring.customProxy;
import java.lang.reflect.Method;
public interface MyInvocationHandler{
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

代理成功!

通过这个案例就理解了JDK动态代理的原理,真正是如何实现的

复习一下原理

拿到被代理对象的引用(张三类),然后获取它实现的接口接口也就是Person
JDK代理重新生成一个类,同时实现我们给的代理对象所实现的接口 (Person接口)给每个方法内部实现都写成this.h.invoke,这样就变成了调用中介类的invoke,当然也会把实际需要调用的方法名传入给中介的invoke以便执行完代理逻辑前/后调用真正的方法

总结一下有点像狸猫换太子:新生成一个类,类里的所有方法都去调用代理类的那个invoke方法,同时把真正需要调用的方法名也传入。 然后代理类的invoke就被执行了,然后代理类的invoke在执行代理逻辑时再调用下真实那个被代理类的那个方法

总结:字节码重组

上面的JDK的动态代理一定要写一个接口,例如案例中的Person(张三实现了Person接口,所以在张三类中可以拿到它实现的接口也就是Person),因为我们生成的class文件需要实现这个接口,所以必须要这个接口,否则实现不了。CGlib不需要接口就可以实现动态代理

代理的实现分动态代理静态代理

静态代理的实现是对已经生成了的JAVA类进行封装。

动态代理则是在运行时生成了相关代理类,在JAVA中生成动态代理一般有两种方式

  • JDK实现代理生成,是用类 java.lang.reflect.Proxy, 实现方式 就是上面这个案例
  • 用CGLib实现 CGLIB是一个开源项目

Spring中,bean都是由动态代理生成出来的 Spring AOP中,当拦截对象实现了接口时,生成方式是用JDK的Proxy类。当没有实现任何接口时用的是GCLIB开源项目生成的拦截类的子类. Spring是集成了CGLib的

CGLib的实现原理: 自动生成一个类,继承自己的那个被代理类,然后把子类的引用指向父 类 ,同样是做了字节码重组,

好处就是可以少写一个接口 ,对于使用API的用户来说是无感知的

CGLib案例

Zhangsan.java 张三

public class Zhangsan {
    public void findHouse(){
        System.out.println("我要找房子");
    }
}

中介 Zhongjie.java

public class Zhongjie implements MethodInterceptor {//interceptor的英文意思是拦截机
    public Object getInstance (Class clazz) throws  Exception{
        //通过反射机制,实例化
        Enhancer enhancer=new Enhancer();
        //把父类设置为谁?
        enhancer.setSuperclass(clazz);//传入被代理对象的Class,这一步就是告诉cglib,生成的子类需要继承哪个类
        enhancer.setCallback(this);//设置回调 后续this.h.invoke调用的是本身的intercept方法
        return enhancer.create();//第一步生成源代码,第二步编译Class,第三步加载到JVM中,并返回被代理后的对象

    }
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {//这里的obj并不是我们new出来的被代理对象的引用,而是被代理对象的class
        System.out.println("代理开始");
        System.out.println("被调用的方法名:"+method.getName());
        //这个obj的引用是由CGLib给我们new出来的,CGLib new出来以后的对象是被代理对象的子类(继承了我们自己写的那个被代理对象也就是Zhangsan.java)
        //new子类的同时,必须先new出父类来,这就相当于是间接的持有了我们父类的引用,子类重写了父类的所有的方法,变成类似于this.h.invoke(...),我们改变子类对象的某些属性是可以间接的操作父类的属性的
        Object result=methodProxy.invokeSuper(obj,args);//一定是调Super,这里调用Super其实就是原始的那个被代理对象的那个原始方法.不能写Invoke不然会死循环 相当于在findHose中调用findHouse
        System.out.println("代理结束");
        return result;
    }
}

测试类,main方法

public static void main(String[] args) {
        //JDK的动态代理是通过接口来进行强制转换的,生成以后的代理对象可以强制转换为接口
        //CGLib的动态代理是通过生成一个被代理对象的子类,然后重写父类的方法,生成以后的对象时可以强制转换为被代理对象,子类引用赋值给父类
        Zhongjie zhongjie =new Zhongjie();
        try{
            //Zhangsan zhangsan=(Zhangsan)zhongjie.getInstance(new Zhangsan());//其实没用到这个对象的引用,只需要它的class,这样写只是方便,在Zhongjie.java中也是obj.getClass,为了不避免误会,采用下面的方式
            Zhangsan zhangsan=(Zhangsan)zhongjie.getInstance(Zhangsan.class);
            zhangsan.findHouse();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

代理模式用的最多的就是SpringAOP

可以做什么事情?可以在每一个方法调用之前加一些代码,在方法调用之后再加一些代码

AOP一般用在哪里 :事务代理(声明式事务,哪个方法需要加事务,哪个方法不需要加事务s)、日志

在调用service方法之前开启(open)一个事务,事务的执行是由我们自己的代码完成的,事务执行完成后监听是否有异常,可能要根据异常的类型来决定这个事务是否要回滚(rollaback)还是继续提交(commit),最后事务还要关闭(close)

上面的流程中,除了事务的执行(加粗部分),其他操作都是AOP帮我们做的

AOP是通过代理这种技术来实现的,AOP不是代理,代理也不是AOP

何时Spring使用JDK/CGLib代理

当有接口时使用JDK动态代理,没有时使用CGLib动态代理。

当proxyTargetClass为true是不管什么情况都使用CGLib

扩展阅读:

十分钟理解Java之动态代理

https://www.jianshu.com/p/942a4a349ac4

https://www.jianshu.com/p/774c65290218

https://www.cnblogs.com/lianghaoc/p/5699537.html

https://segmentfault.com/a/1190000007089902

This blog is under a CC BY-NC-SA 3.0 Unported License
本文链接:http://hogwartsrico.github.io/2018/09/01/proxy-in-Spring/