深入理解spring(代理模式源码1)五

(一)AOP 原理解析

众生周知,AOP实现原理是基于动态代理。

什么是代理?

增强一个类,或者一个对象的功能。就可以说是代理。
如:买火车票? app 12306 就是一个代理,他代理了火车站。

java实现代理的两种方式:

代理的名词:

代理对象 :增强后的对象
目标对象 :被增强的对象
他们的身份不是绝对的,会根据情况发生变化

1.1静态代理
继承

代理对象继承目标对象,重写需要增强的方法。完成静态代理!调用即可。。面向接口编程就是静态代理
继承方式代理会有一个问题。每次增强都需要继承。。如果需要多次(如下聚合)增强,会产生很多代理类。相对复杂。
深入理解spring(代理模式源码1)五_第1张图片

聚合

目标对象,和代理对象实现同一个接口。并且代理对象包含 目标对象;
核心示例代码:
传入目标对象,对目标对象增强!切换不同的类,就可以对不同的类完成代理。

//代理对象1
public class UserDaoLog implements UserDao {
    //装饰者模式,传入对象
    private UserDao dao;
    public UserDaoLog(UserDao dao){
        this.dao=dao;
    }
    /**
     * 对目标对象做增强处理
     */
    @Override
    public void query() {
        System.out.println("打印日志。");
        dao.query();
    }
 //代理对象2
public class UserDaoTime implements UserDao {
    //装饰者模式,传入对象
    private UserDao dao;
    public UserDaoTime(UserDao dao){
        this.dao=dao;
    }

    /**
     * 对目标对象做增强处理
     */
    @Override
    public void query() {
        System.out.println("打印时间。");
        dao.query();
    }
 }
    //传入目标对象完成代理,要代理其他类,只需要切换对象。
    如果老板说,既要完成打印时间,又要完成打印日志该怎么办呢?
public static void main(String[] args) {
        UserDao dao = new UserDaoImpl();
//        UserDao prox = new UserDaoTime(dao);
        UserDao prox = new UserDaoLog(dao);
        prox.query();
    }

思考:如果我们要完成多重代理呢?改怎么办?(既要完成打印时间,又要完成打印日志)

1.1.1 多重代理(多次增强!)

对多个代理对象,代理。。完成双重代理。(如下代码片段,只需要传入 UserDaoLog+UserDaoImpl)
然后再对他们做代理。。继承的方式 就会产生很多类。聚合实现起来就更加简单。

    public static void main(String[] args) {
        UserDao traget = new UserDaoLog(new UserDaoImpl());
        UserDao prox = new UserDaoTime(traget);
        prox.query();
    }
    ---------------------控制台输出------------------------
    打印时间。
	打印日志。
	假装查询数据库
假设这个时候,老板要求先打印日志,再打印时间呢?
    public static void main(String[] args) {
        UserDao traget = new UserDaoTime(new UserDaoImpl());
        UserDao prox = new UserDaoLog(traget);
        prox.query();
    }
    ---------------------控制台输出------------------------
	打印日志。
	打印时间。
	假装查询数据库
缺点:如上,我们只是对一个类(userDao)做代理,如果对多个类做代理呢?就算用聚合的方式一样会产生很多类。只不过比继承少一点,如上每个类做一次双重代理,就需要两个代理类。这个问题该怎么解决呢?
总结:如果在不确定的情况下,尽量不要使用静态代理。因为一但写代码就会产生类。一但产生类,就会写到爆炸。那什么场景会用到静态代理呢?JDK中的IO流,BufferIO流,其实都是使用的装饰者模式。就是静态代理的方式。

1.2 手写动态代理

面试官:什么是动态代理?
初中级开发:基于java反射,增强啊,调用者和实现者之间解耦啊。张口就来了
网上博客:1:抽象类接口,2:被代理类(具体实现抽象接口的类),3:动态代理类:实际调用被代理类的方法和属性的类。InvocationHandler 方法啊等等等,都是只懂皮毛
思考:先抛开以上的所有想法,如果我们不想创建任何类的情况下要增强一个类,该怎么办?

我们是不是得先有一个类?去继承或者实现目标对象?才能去反射?可是我们连类都没有,拿什么去反射?所以反射只是动态代理的冰山一角。那么现在我们不能在项目中去创建一个类,该怎么办?可以把这个类定义在代码中吗?不管可不可以我们先来试一下

如下:假设我们要对UserDaoImpl 做代理。如下图所示,我们已经得到了要代理UserDaoImpl的所有内容

public class UserDaoImpl implements UserDao{
    public void query(){
        System.out.println("假装查询数据库");
    }
    public String query(String aa){
        return aa;
    }
  }

 public static void main(String[] args) {
        //获取UserDaoImpl实现的接口 我们这是山寨版的动态代理,暂时只处理第一个接口
        Class targetInf = new UserDaoImpl().getClass().getInterfaces()[0];
        //获取到接口的方法
        Method methods[] = targetInf.getDeclaredMethods();
        //定义换行
        String line = "\n";
        //定义tab空格
        String tab = "\t";
        //获取到实现的接口名字 这里就是UserDao
        String infName = targetInf.getSimpleName();
        String content = "";
        //我们要写一个类的类容 第一行是不是就是外面的包名?这里就写的叼一点google
        String packageContent = "package com.google;" + line;
        //第二行 是不是我们要导包?类.getName是不是就得到了类的全路径 com.luban.dao.UserDao
        String importContent = "import " + targetInf.getName() + ";" + line;
        //创建一个类,类名为$Proxy 实现了接口UserDao
        String clazzFirstLineContent = "public class $Proxy implements " + infName + "{" + line;
        //定义私有变量 private UserDao target;
        String filedContent = tab + "private " + infName + " target;" + line;
        //定义构造方法 传入 UserDao target 并且给私有成员变量赋值
        String constructorContent = tab + "public $Proxy (" + infName + " target){" + line
                + tab + tab + "this.target =target;"
                + line + tab + "}" + line;
        String methodContent = "";
        //遍历接口的所有方法
        for (Method method : methods) {
            //获取到方法的返回类型
            String returnTypeName = method.getReturnType().getSimpleName();
            //获取到方法名称
            String methodName = method.getName();
            // 获取到方法的入参类型,如Sting.class String.class
            Class argse[] = method.getParameterTypes();
            String argsContent = "";
            String paramsContent = "";
            int flag = 0;
            for (Class arg : argse) {
                //遍历入参类型数组,获取到入参的对象名称 如 String
                String temp = arg.getSimpleName();
                System.out.println(temp);
                //拼装参数 如 ,String p0,Sting p1,
                argsContent += temp + " p" + flag + ",";
                //拼装形参(需要调用父类方法用到) 如 , p0, p1,
                paramsContent += "p" + flag + ",";
                flag++;
            }
            //截取掉参数的最后一个逗号
            if (argsContent.length() > 0) {
                argsContent = argsContent.substring(0, argsContent.lastIndexOf(",") - 1);
                paramsContent = paramsContent.substring(0, paramsContent.lastIndexOf(",") - 1);
            }
            //我们有了 返回类型,有了方法名,有了入参,还有了调用父类的方法。就可以拼装 实现的方法
            methodContent += tab + "public " + returnTypeName + " " + methodName + "(" + argsContent + ") {" + line;
            //判断返回值不等于void return 
         if (!returnTypeName.equals("void")) {
                        methodContent+=tab + tab + "System.out.println(\"带参数测试增强打印log\");" + line
                                + tab + tab + "return target." + methodName + "(" + paramsContent + ");" + line
                                + tab + "}" + line;
                    }else {
                        methodContent+= tab + tab + "System.out.println(\"测试假装增强打印log\");" + line
                                +tab + tab + "target." + methodName + "(" + paramsContent + ");" + line
                                + tab + "}" + line;
                    }
        }
        //最后把全部加起来。就是外面一个类里面的内容
        content = packageContent + importContent + clazzFirstLineContent + filedContent + constructorContent + methodContent + "}";
        System.out.println(content);
    }
---------------- ---------------------控制台输出-------------------------------------------------
 package com.google;
	import com.luban.dao.UserDao;
	public class $Proxy implements UserDao{
		private UserDao target;
		public $Proxy (UserDao target){
			this.target =target;
		}
		public void query() {
			System.out.println("测试假装增强打印log");
			target.query();
		}
		public String query(String p) {
			System.out.println("测试假装增强打印log");
			return target.query(p);
		}
	}
如上图,我们已经得到了要代理UserDaoImpl代码内容,得到了代码内容我们是不是还得将内容产生一个.java文件?是不是还得编译.java文件得到.class文件?是不是还得new 这个对象 才能完成代理?
思考一下,我们怎么把内容变成.java文件呢?是不是可以用IO流的字符流?
1、代码接上面部分。(伸手党拷贝的同学注意了)如下,我们把内容写到G盘上产生.java文件。并且编译它。
		//代码接上面部分+
		File file1 =new File("G:\\com\\google");
        File file =new File("G:\\com\\google\\$Proxy.java");
            //判断是否是一个目录
            if (!file1.isDirectory())
                //创建目录
                file1.mkdirs();
            if (!file.exists())
                //创建文件
                file.createNewFile();
            //将字符串 写出到磁盘
            FileWriter fw = new FileWriter(file);
            fw.write(content);
            fw.flush();
            fw.close();
        //引用java编译器。编译java文件
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        //传入要编译的文件
        Iterable units = fileMgr.getJavaFileObjects(file);
        JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
        t.call();
        fileMgr.close();
在G盘上就产生了一个 $Proxy.java的文件与.class文件,内容如下。

深入理解spring(代理模式源码1)五_第2张图片

2.接下来我们.java 文件也有了,.class文件也有了,我们怎么得到这个对象呢?这些代码都在我们的本地磁盘中,怎么加载到我们的项目中呢?还记得我们的类加载器嘛?这里就派上用场了。

1、代码接上面部分。(伸手党拷贝的同学注意了)

//创建需要加载.class的地址
        URL[] urls = new URL[]{new URL("file:G:\\\\")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        //加载class对象
        Class clazz = urlClassLoader.loadClass("com.google.$Proxy");
        //反射获取到构造方法 构造方法带参数,所以传入接口对象
        Constructor constructor = clazz.getConstructor(targetInf);
        //构造方法传入目标对象 就拿到了增强后的代理对象
        UserDao proxy = (UserDao)constructor.newInstance(new UserDaoImpl());
        System.out.println(proxy);
        //代理对象调用无参方法
        proxy.query();
        //代理对象调用有参方法
        System.out.println(proxy.query("哈哈哈哈哈哈哈哈测试成功"));
---------------- ---------------------控制台输出-------------------------------------------------
com.google.$Proxy@482f8f11
测试假装增强打印log
假装查询数据库
带参数测试增强打印log
哈哈哈哈哈哈哈哈测试成功

总结:上面我们分析了,静态代理与动态代理。并且手写了一个山寨版本的动态代理。大家应该对动态代理有了 不一样的理解。大家可能觉得很low 那么JDK的动态代理是否也是这样做的呢?其实JDK的动态代理 原理大致上也是这样做的。下一篇,我们会来解析 JDK的动态代理源码。。

项目源码地址:源码地址

你可能感兴趣的:(spring源码学习)