zoukankan      html  css  js  c++  java
  • 如果让我写代理的实现

     

    如果让我写代理的实现

     

     

    代理实现的思考

    场景案例

    以一个简单的计算器为例,我们先定义好计算器的简单接口如下:

    package interfaces;
    
    
    public interface ICalculator {
        public int add(int i, int j);
    
        public int sub(int i, int j);
    
        public int mul(int i, int j);
    
        public int dev(int i, int j);
    }

    然后我们写一个实现类和测试类:

    package impl;
    
    import interfaces.ICalculator;
    
    public class MyCalculator1 implements ICalculator {
        @Override
        public int add(int i, int j) {
            int result = i + j;
            return result;
        }
    
        @Override
        public int sub(int i, int j) {
            int result = i - j;
            return result;
        }
    
        @Override
        public int mul(int i, int j) {
            int result = i * j;
            return result;
        }
    
        @Override
        public int dev(int i, int j) {
            int result = i / j;
            return result;
        }
    }
    @Test
    public void testAdd() {
        MyCalculator1 myCalculator1 = new MyCalculator1();
        myCalculator1.add(3, 4);
    }

    现在我们的计算器类能够实现四则运算,业务需求是完成了。但是在业务需求之外,我想在整个系统层面加上日志记录,也就是说在业务层的每个方法里加入日志记录,我就需要手动的把一些和业务需求无关的代码嵌入到业务中,如下:

    package impl;
    
    import interfaces.ICalculator;
    
    public class MyCalculator2 implements ICalculator {
        @Override
        public int add(int i, int j) {
            System.out.println("参数:i=" + i + ";j=" + j);
            int result = i + j;
            System.out.println("运算结果:" + result);
            return result;
        }
    
        @Override
        public int sub(int i, int j) {
            System.out.println("参数:i=" + i + ";j=" + j);
            int result = i - j;
            System.out.println("运算结果:" + result);
            return result;
        }
    
        @Override
        public int mul(int i, int j) {
            System.out.println("参数:i=" + i + ";j=" + j);
            int result = i * j;
            System.out.println("运算结果:" + result);
            return result;
        }
    
        @Override
        public int dev(int i, int j) {
            System.out.println("参数:i=" + i + ";j=" + j);
            int result = i / j;
            System.out.println("运算结果:" + result);
            return result;
        }
    }

    日志记录和计算逻辑并不关系,这就导致了系统需求和业务需求代码耦合在了一起。我又突然发现,上面的日志记录好像没有记录方法名称,如果要修改的化,我要手动的把每个方法里的日志记录代码都更改一下,如果系统中这种耦合非常多的话,维护起来费时费力。

    为了维护起来方便,我们可以单独的把日志记录抽取成一个静态类,这样我们只需要在需要加日志的地方调用即可,如下先写我们的静态日志记录类:

    package impl;
    
    import java.util.Arrays;
    
    public class LogUtils {
        public static void start(Object... obj) {
            System.out.println("参数:" + Arrays.asList(obj));
        }
    
        public static void end(Object obj) {
            System.out.println("运算结果:" + obj);
        }
    }

    然后我们的计算类改成使用静态类来记录日志:

    package impl;
    
    import interfaces.ICalculator;
    
    public class MyCalculator3 implements ICalculator {
        @Override
        public int add(int i, int j) {
            LogUtils.start(i, j);
            int result = i + j;
            LogUtils.end(result);
            return result;
        }
    
        @Override
        public int sub(int i, int j) {
            LogUtils.start(i, j);
            int result = i - j;
            LogUtils.end(result);
            return result;
        }
    
        @Override
        public int mul(int i, int j) {
            LogUtils.start(i, j);
            int result = i * j;
            LogUtils.end(result);
            return result;
        }
    
        @Override
        public int dev(int i, int j) {
            LogUtils.start(i, j);
            int result = i / j;
            LogUtils.end(result);
            return result;
        }
    }

    日志类中我们使用可变参数的方式,可以接受多个参数的情况,如果参数的类型是Method类型,我们就知道传递的这个参数是方法,这样我们就可以方便的记录方法名称了,但是如果真的要记录方法,我们依旧需要手动的在业务代码中调用start方法时,把Method参数传递进来,并没有解决我们上面的那个问题,而且业务需求和系统需求的代码依旧耦合在一起。这是AOP吗?AOP的概念是业务逻辑和系统逻辑不应该耦合在一起,而是在编译期或运行期将系统逻辑注入到业务逻辑的切入点中。

    我们希望的是日志功能能够在业务功能运行期间自己主动添加上,那么首先我们肯定不能用真实的类对象,因为真实的类对象的代码是固定的,里面并没有日志相关的代码,那我们只好使用代理类对象去代理真实的类对象的行为,这样我们可以在代理对象调用真实类对象行为方法执行的前后加入日志逻辑,也就实现了所谓的AOP。

     

     

    代理实现的思考

    如果让我们来实现代理的技术,那我们需要知道什么时候调用了被代理对象的什么方法,然后把相关的日志逻辑织入到被代理对象执行的某个方法前后。怎么才能知道什么时候调用了被代理对象的什么方法呢?

    首先进入脑海的想法就是我们如果有一个十分强大的代理类,不管什么对象它都可以代理,这么一个通用且强大的代理类就叫做Proxy类吧,这个类是所有需要被代理类的一个代表,比如说:你是做开发的,你旁边的妹纸是做策划的,客户来做项目一般不会来找你俩,而是找公司的商务,商务和客户谈判,这里的商务相当于一个代表,也就是Proxy。当你需要一个代理对象的时候,到时候我就直接返回给你这个Proxy的对象就行了。但是代理对象怎么样才能和被代理对象有同样的行为呢?如果不能做同样的行为,代理就不能称之为代理。我们自然的可以想到使用extends或者implements,如果代理类继承了被代理类,或者代理类实现了被代理类所实现的接口,那么代理类自然就和被代理类具有了相同的行为:

    1、extends方式:如果是代理类继承被代理类,前提条件就是代理类不能真实的在硬盘中存在,它应该是一个在内存中临时生成的新类,如果是在硬盘中存在,当A需要一个代理时你修改了硬盘中真实存在的代理类继承自A类,那对于B想用代理类时,你再让代理类继承自B类,由于不能使用多继承,显然是行不通的。

    2、implements方式:如果是代理类实现了被代理类所实现的接口,就需要要求被代理类必须有某个接口的实现。这样我们可以让硬盘中真实存在的代理类去多重实现以达到和所有被代理类具有相同行为的的目的,但是细想这岂不是很扯?你一个商务本来是代表程序员去和客户谈产品的,多重实现之后,你既要代表程序员根客户谈产品又要代表保洁阿姨打扫卫生。所以不管是采用extends还是implements方式,这个代理类都应该在内存中临时生成一个新的,大家可以共用一套代理类的模板去生成一个新代理类,每个人按照自己的需求可以修改这个新类,这样的话你用你的新代理类,我用我的新代理类,不至于混乱。

    好了,现在先不管代理类是采用哪种方式达到和被代理类具有相同行为的目的,我们明确一点的是,都要采用某一套模板在运行时动态生成一个新类了,那这一套模板我们该怎么表述?

    既然动态生成的新类是基于这一套模板的,那我们以面向对象的思想来说,模板应该是一个类或接口,这个模板肯定不能每次启动虚拟机临时生成吧,再说了这一套基本模板大家应该都是一样的,所以它应该存在于磁盘上,可序列化,既然是一套模板如果把他定义成接口,岂不是很扯,我们知道生成一个类肯定要做很多步骤,你仅仅定义接口的话,接口的方法里不能有任何内容啊,你这些步骤在哪实现?所以这个模板应该用类来描述比较恰当。模板用一个类来描述的话,那被代理类和模板应该有一个怎样的关系呢?分析有以下两种可能关系:

    1、如果把模板看做基类的话,动态新生成的代理类如果一旦继承了这个模板基类,那么它就不能采用extends方式了,会导致多继承,只能采用implements方式。

    2、如果把模板看做是一个静态工具类,我们动态新生成的代理类只是使用了这个工具类创建的,那这个代理类既可以采用extends方式也可以采用implements方式。

    如果采用关系1,使用implements的方式实现代理我们就不得不要求程序员们,在使用我们发明的这套技术时被代理的类一定要实现了某个接口才行。新生成的代理类应该继承于模板然后实现真实类所实现的接口,如果用代码表示就应该像下面这样:

    public final class $Proxy0 extends Proxy
      implements interfaces.ICalculator
    {

    上面的Proxy就是我所指的模板基类,$Proxy0就是在新生成的代理类,ICalculator也就是MyCalculator1类所实现的接口了。

    如果采用关系2,我们创建代理类就相当灵活了。

    总结一下,从上到下推理,现在得出了两种结论,①把模板看作基类,动态生成的类继承自模板类,且实现被代理类所实现的接口。②不把模板看作基类,只把他当做一个用于动态生成新代理类的静态方法,这样我们动态生成的新类,既可以采用extends方式,也可以implements方式。如果说被代理类没有实现的接口,我们这种方式也可以实现代理。

    那现在结论是有了,要具体实现这两种结论都要面临一个问题,怎么样在运行时动态的创建一个没有字节码的新类?这可难倒了我,要知道怎么创建一个类,你首先要知道啥是类,也就是说class的文件结构是啥样的,咱们只好回头再次翻看一下Java虚拟机规范是怎么定义Class文件了,我大致翻看了一下JavaSE7的第四章Class文件格式,我尼玛,ClassFile结构、描述符和签名、常量池、字段、方法、属性、约束、校验、虚拟机限制...这里面写的实现方式都不是用Java语言实现的吧,虽然我实现不出来,但现在我们知道上面推出的两种结论都是可行的,如果虚拟机提供了某种操作,可以方便的让我们去动态创建一个class文件的话就好了(JDK动态代理,采用了结论①),或者由谁写一个操作字节码的库,最后能输出我们想要的class文件也行(CGLib和Jaavssist两种采用了结论②,它们都提供了比较高级的API来操作字节码,最后输出class文件)。

     

     

    使用JDK动态代理

    要使用JDK的动态代理,必不可少的,被代理的类要实现某个接口,正如我们上面的案例,MyCalculator1实现了ICalculator这个接口,能满足这个条件。现在我们只需要按照JDK动态代理提供的API方式调用就行了:

    package impl;
    
    
    import interfaces.ICalculator;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    public class CalculatorJDKProxy {
    
        /**
         * 注意动态代理invoke方法传入的是接口
         * @param iCalculator
         * @return
         * @throws Throwable
         */
        public static ICalculator getProxy(final ICalculator iCalculator) throws Throwable {
            // 方法执行器,帮我们目标对象执行目标方法
            InvocationHandler h = new InvocationHandler() {
                /**
                 *
                 * @param proxy:代理对象,给jdk使用,任何时候都不要动这个对象
                 * @param method:当前要执行的目标对象的方法
                 * @param args:要执行目标方法的参数
                 * @return
                 * @throws Throwable
                 */
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("JDK动态代理:目标方法被执行了");
                    Object obj = method.invoke(iCalculator, args);
                    return obj;
                }
            };
            // 目标对象实现了哪些接口
            Class<?>[] interfaces = iCalculator.getClass().getInterfaces();
            // 目标对象的类加载器
            ClassLoader loader = iCalculator.getClass().getClassLoader();
            Object proxy = Proxy.newProxyInstance(loader, interfaces, h);
            System.out.println(proxy.getClass());
            return (ICalculator) proxy;
        }
    }

    测试JDK动态代理:

    @Test
    public void testJDKProxy() throws Throwable {
        ICalculator proxy = CalculatorJDKProxy.getProxy(new MyCalculator1());
        proxy.add(3, 4);
    }

    原理上面也说过了,JDK的动态代理是基于implements实现的,也就是说新生成的代理类继承了Proxy(上面说的模板)实现了ICalculator接口,在代理类执行ICalculator接口的某个方法时,利用反射调用被代理类的方法(method.invoke),我们可以在调用被代理类对象的方法之前插入一些逻辑,例如日志等。

     

    使用CGLib动态代理

    package impl;
    
    import org.springframework.cglib.proxy.Enhancer;
    import org.springframework.cglib.proxy.MethodInterceptor;
    import org.springframework.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    
    public class CalculatorCGLibProxy {
    
        /**
         * 入参只要是一个非抽象类就可以,代理抽象类没有意义
         *
         * @param cls
         * @return
         * @throws Throwable
         */
        public static Object getProxy(Class cls) throws Throwable {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(cls);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                    System.out.println("CGLib动态代理:目标方法被执行了");
                    Object result = methodProxy.invokeSuper(proxy, args);
                    return result;
                }
            });
            return enhancer.create();
        }
    }

    测试CGLib动态代理:

    @Test
    public void testCGLibProxy() throws Throwable {
        MyCalculator1 myCalculator1 = (MyCalculator1) CalculatorCGLibProxy.getProxy(MyCalculator1.class);
        myCalculator1.add(3, 4);
    }

    使用CGLib动态代理改进计算器

    package impl;
    
    import java.lang.reflect.Method;
    import java.util.Arrays;
    
    public class LogUtil {
        public static void _start(Method method, Object[] args) {
            System.out.println("【" + method.getName() + "】 方法开始执行,参数列表: " + Arrays.asList(args));
        }
    
        public static void _end(Method method, Object result) {
            System.out.println("【" + method.getName() + "】 方法结束执行,返回值: " + result);
        }
    
        public static void _return(Method method) {
            System.out.println("【" + method.getName() + "】 方法retuen");
        }
    
        public static void _except(Method method, Throwable e) {
            System.out.println("【" + method.getName() + "】 方法异常,异常原因:" + e);
        }
    }

     

    package impl;
    
    import org.springframework.cglib.proxy.Enhancer;
    import org.springframework.cglib.proxy.MethodInterceptor;
    import org.springframework.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    
    public class CalculatorCGLibProxy {
        
        public static Object getProxy(Class cls) throws Throwable {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(cls);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                    Object result = null;
                    try {
                        LogUtil._start(method, args);
                        result = methodProxy.invokeSuper(proxy, args);
                        LogUtil._end(method, result);
                    } catch (Exception e) {
                        LogUtil._except(method, e);
                    }
                    LogUtil._return(method);
                    return result;
                }
            });
            return enhancer.create();
        }
    }

     

    前进时,请别遗忘了身后的脚印。
  • 相关阅读:
    java excel转pdf 工具类
    java word转pdf 工具类
    如何向数据库中添加TIMESTAMP(6)类型的数据
    IE浏览器 div或者其他容器的height属性无效 滚动条问题解决办法
    ComboBox赋值ItemsSource数据源的时候会触发SelectionChanged改变事件的解决办法
    devexpress chart 散点图加载并分组显示(可以自定义颜色 同组中的点颜色相同)
    myEclipse mybatis自动生成工具xml配置
    MySQL日志简介
    存储引擎简介
    索引介绍
  • 原文地址:https://www.cnblogs.com/liudaihuablogs/p/14332554.html
Copyright © 2011-2022 走看看