• 从零开始写JavaWeb框架(第四章节的AOP)


    使用"链式代理"实现 AOP

     

    本文是《轻量级 Java Web 框架架构设计》的系列博文。

    大家是否还记得《Proxy 那点事儿》中提到的 CGLib 动态代理吗?我就是使用这个工具来实现了 Smart AOP 的,原以为这样 AOP 就轻松搞定了,但万万没想到的是,自己太傻太天真。

    昨天刚发现 Smart AOP 的 AOPHelper 类有个严重的 Bug,导致同一个目标类不能同时被多个切面类横切,运行时会报错。深入了解才知道,如果使用 CGLib 写了一个增强类,该增强类就不能再次被 CGLib 自己进行增强。换言之,CGLib 不支持嵌套增强!

    我喜欢用一个简单的示例来说明一个深刻的道理,请往下看,请确保您有足够的耐心与探索的欲望。系好安全带吧!

    还是有个接口:Greeting

    public interface Greeting {
    
        void sayHello(String name);
    }
    

    仍然有个实现类:GreetingImpl

    public class GreetingImpl implements Greeting {
    
        @Override
        public void sayHello(String name) {
            System.out.println("Hello! " + name);
        }
    }
    

    恐怕这个例子已经在我的博客中出现过无数次了,但百看不厌。

    我们知道,CGLib 可以代理目标类,而该对目标类有无接口根本就不在意。所以我们忽略掉 Greeting 接口吧,直接面向实现编程(故意违背一下“依赖倒置原则”)。

    下面,我要写两个 Proxy,一个是 BeforeProxy(用于实现“前置增强”),另一个是 AfterProxy(用于实现“后置增强”)。这些所谓的“增强类型”其实都是 AOP 的术语,对 AOP 需要温习一下的同学,回头可阅读这篇博文《AOP 那点事儿》。

    先上一个 BeforeProxy:

    public class BeforeProxy implements MethodInterceptor {
    
        public Object getProxy(Class cls) {
            return Enhancer.create(cls, this);
        }
    
        @Override
        public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            before();
            return proxy.invokeSuper(target, args);
        }
    
        private void before() {
            System.out.println("Before");
        }
    }
    

    再搞一个 AfterProxy:

    public class AfterProxy implements MethodInterceptor {
    
        public Object getProxy(Class cls) {
            return Enhancer.create(cls, this);
        }
    
        @Override
        public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            Object result = proxy.invokeSuper(target, args);
            after();
            return result;
        }
    
        private void after() {
            System.out.println("After");
        }
    }
    

    下面用一个 Client 类完成“BeforeProxy + AfterProxy”的功能,也就是说,我想同时让以上两个 Proxy 去增强目标类 GreetingImpl。

    public class Client {
    
        public static void main(String[] args) {
            Object greetingImplBeforeProxy = new BeforeProxy().getProxy(GreetingImpl.class);
            Object greetingImplAfterBeforeProxy = new AfterProxy().getProxy(greetingImplBeforeProxy.getClass());
    
            GreetingImpl greetingImpl = (GreetingImpl) greetingImplAfterBeforeProxy;
    
            greetingImpl.sayHello("Jack");
        }
    }
    

    应该就是这样写吧?先用 BeforeProxy 去增强 GreetingImpl,得到一个代理对象 greetingImplBeforeProxy。然后再用 AfterProxy 去增强上一步得到的代理对象,得到一个新的代理对象 greetingImplAfterBeforeProxy。最后将这个新的代理对象强制转型为目标类对象,并调用目标类对象的 sayHello() 方法。

    这样能行吗?反正编译是没有报错的,管它的,老子先运行一把再说。

    Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
    at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)
    at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
    at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
    at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:663)
    at com.smart.lab.proxy.cglib_proxy.AfterProxy.getProxy(AfterProxy.java:11)
    at com.smart.lab.proxy.cglib_proxy.Client.main(Client.java:9)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
    Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:384)
    at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:219)
    ... 10 more
    Caused by: java.lang.ClassFormatError: Duplicate method name&signature in class file com/smart/lab/proxy/GreetingImpl$$EnhancerByCGLIB$$76346a2$$EnhancerByCGLIB$$8930dbed
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
    ... 16 more

    运行时报错!而且抛出了一个 net.sf.cglib.core.CodeGenerationException,可见这是 CGLib 的内部异常。从 Caused by 中可见,报错信息是“Duplicate method name&signature”(重复的方法签名)。遇到这种异常,一般都是很让人抓狂。

    看来 CGLib 自动生成的类,不能被自己再次增强了。证实了我之前说的那一点:CGLib 不支持嵌套增强!

    Smart AOP 中也会遇到以上的报错现象,说明如果解决了 CGLib 的嵌套增强问题,也就解决了 Smart AOP 的问题。

    如何解决呢?下面才是精华!请检查一下您的安全带是否系牢?

    不妨借鉴 Servlet 的 Filter Chain 的设计模式,它是“责任链模式”的一种变体,在 JavaEE 设计模式中命名为“拦截过滤器模式”。我们可以将每个 Proxy 用一根链子串起来,形成一个 Proxy Chain。然后调用这个 Proxy Chain,让它去依次调用 Chain 中的每个 Proxy。

    第一步:定义一个 Proxy 接口

    public interface Proxy {
    
        void doProxy(ProxyChain proxyChain);
    }
    

    该接口中,只有一个 doProxy() 方法,该方法中关联了 ProxyChain 这个类,这样做是为了让 ProxyChain 来调用 Proxy。

    第二步:编写 ProxyChain 类

    public class ProxyChain {
    
        private List<Proxy> proxyList;
        private int currentProxyIndex = 0;
    
        private Class<?> targetClass;
        private Object targetObject;
        private Method targetMethod;
        private Object[] methodParams;
        private MethodProxy methodProxy;
        private Object methodResult;
    
        public ProxyChain(Class<?> targetClass, Object targetObject, Method targetMethod, Object[] methodParams, MethodProxy methodProxy, List<Proxy> proxyList) {
            this.targetClass = targetClass;
            this.targetObject = targetObject;
            this.targetMethod = targetMethod;
            this.methodParams = methodParams;
            this.methodProxy = methodProxy;
            this.proxyList = proxyList;
        }
    
        public Class<?> getTargetClass() {
            return targetClass;
        }
    
        public Object getTargetObject() {
            return targetObject;
        }
    
        public Method getTargetMethod() {
            return targetMethod;
        }
    
        public Object[] getMethodParams() {
            return methodParams;
        }
    
        public MethodProxy getMethodProxy() {
            return methodProxy;
        }
    
        public Object getMethodResult() {
            return methodResult;
        }
    
        public void doProxyChain() {
            if (currentProxyIndex < proxyList.size()) {
                proxyList.get(currentProxyIndex++).doProxy(this);
            } else {
                try {
                    methodResult = methodProxy.invokeSuper(targetObject, methodParams);
                } catch (Throwable throwable) {
                    throw new RuntimeException(throwable);
                }
            }
        }
    }
    

    该类的代码量比较大(竟然有 50 多行),总结一下都做了什么吧:

    1. 定义了一个 List<Proxy> proxyList,用于封装所有的 Proxy,它就是 ProxyChain 的数据载体。
    2. 定义了一个 int currentProxyIndex,相当于 proxyList 的当前指针,后面可以看到,它是可以往后走动的。
    3. 定义了一组目标类与目标方法相关的数据,其实这些数据都是来自于 CGLib 框架,后面可以看到。
    4. 提供了一个构造器,实现以上定义的成员变量的初始化工作。
    5. 提供了一组 getter 方法,用于获取目标类与目标方法的成员变量。
    6. 提供了一个 doProxyChain() 方法。下面详细解释这一步。

    先判断当前指针有没有走完所有的 proxyList,若没有走完,则从 proxyList 中获取一个 Proxy,同时让指针往后走一步(指向下一个 Proxy),然后调用 Proxy 的 doProxy() 方法,此时需要将 this(也就是 ProxyChain 实例)传递到该方法中;若已经走完了,使用 CGLib API 调用目标方法,并初始化方法返回值。

    以上步骤中,最难理解的就是最后一步,多看几遍,多思考几遍。如果理解了,您就掌握了该模式的精华!

    第三步:编写 ProxyManager 类

    现在 Proxy 与 ProxyChain 都有了,下面的仍然是精华,我们通过一个“工厂模式”来创建 Proxy。

    public class ProxyManager {
    
        private Class<?> targetClass;
        private List<Proxy> proxyList;
    
        public ProxyManager(Class<?> targetClass, List<Proxy> proxyList) {
            this.targetClass = targetClass;
            this.proxyList = proxyList;
        }
    
        @SuppressWarnings("unchecked")
        public <T> T createProxy() {
            return (T) Enhancer.create(targetClass, new MethodInterceptor() {
                @Override
                public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    ProxyChain proxyChain = new ProxyChain(targetClass, target, method, args, proxy, proxyList);
                    proxyChain.doProxyChain();
                    return proxyChain.getMethodResult();
                }
            });
        }
    }
    

    在 ProxyManager 中,定义了两个成员变量,targetClass 表示目标类,proxyList 也就是代理列表了。通过一个简单的构造器,将这两个成员变量进行初始化。最后提供一个 createProxy() 方法,创建代理对象。在该方法中,封装了 CGLib 的 Enhancer 类,只需提供两个参数:目标类与拦截器。后者在 CGLib 中称为 Callback。特别要注意第二个参数,这里使用了匿名内部类的方式进行实现。

    通过一个匿名内部类来实现 CGLib 的 MethodInterceptor 接口,并填充 intercept() 方法。将该方法的所有入口参数都传递到创建的 ProxyChain 对象中,外加该类的两个成员变量:targetClass 与 proxyList。然后调用 ProxyChain 的 doProxyChain() 方法,可以想象,调用是一连串的,当调用完毕后,可直接获取方法返回值。

    第四步:编写 AbstractProxy 类

    不要忘了,我们的目标不是为了实现 Proxy,而是为了实现 AOP。为了实现 AOP,我采用了“模板方法模式”,弄一个 AbstractProxy 抽象类,让它去实现 Proxy 接口,并在其中定义方法调用模板,在需要横向拦截的地方,定义一些“钩子方法”。Spring 源码中大量使用了这一技巧。

    还等什么?直接上 AbstractProxy 吧!

    public abstract class AbstractProxy implements Proxy {
    
        @Override
        public final void doProxy(ProxyChain proxyChain) {
            Class<?> cls = proxyChain.getTargetClass();
            Method method = proxyChain.getTargetMethod();
            Object[] params = proxyChain.getMethodParams();
    
            begin();
            try {
                if (filter(cls, method, params)) {
                    before(cls, method, params);
                    proxyChain.doProxyChain();
                    after(cls, method, params);
                } else {
                    proxyChain.doProxyChain();
                }
            } catch (Throwable e) {
                error(cls, method, params, e);
            } finally {
                end();
            }
        }
    
        public void begin() {
        }
    
        public boolean filter(Class<?> cls, Method method, Object[] params) {
            return true;
        }
    
        public void before(Class<?> cls, Method method, Object[] params) {
        }
    
        public void after(Class<?> cls, Method method, Object[] params) {
        }
    
        public void error(Class<?> cls, Method method, Object[] params, Throwable e) {
        }
    
        public void end() {
        }
    }
    

    相信您已经看明白了,这里提供了一些列的钩子方法,例如:begin()、filter()、before()、after()、error()、end() 等,这些方法可延迟到子类中去实现,并且实现哪个,完全有用户自己决定。

    下面我们就利用 AbstractProxy 重新实现 BeforeProxy 与 AfterProxy 吧。

    先看 BeforeProxy:

    public class BeforeProxy extends AbstractProxy {
    
        @Override
        public void before(Class<?> cls, Method method, Object[] params) {
            System.out.println("Before");
        }
    }
    

    再看 AfterProxy:

    public class AfterProxy extends AbstractProxy {
    
        @Override
        public void after(Class<?> cls, Method method, Object[] params) {
            System.out.println("After");
        }
    }
    

    是不是比之前的更加简洁了呢?以上搞出了许多类,其实是非常有必要的,它们将一个复杂的问题,简化为多个简单的问题,这就是“ 关注点分离 ”设计原则。

    第五步:编写 Client 类

    好了!您终于快熬到头了,当看到最新的 Client 类,相信您会激动不已!

    public class Client {
    
        public static void main(String[] args) {
            List<Proxy> proxyList = new ArrayList<Proxy>();
            proxyList.add(new BeforeProxy());
            proxyList.add(new AfterProxy());
    
            ProxyManager proxyManager = new ProxyManager(GreetingImpl.class, proxyList);
            GreetingImpl greetingProxy = proxyManager.createProxy();
    
            greetingProxy.sayHello("Jack");
        }
    }
    

    先构造一个空的 List<Proxy> proxyList,然后往里面依次放入需要增强的 Proxy 类,随后使用 ProxyManager 去创建代理实例,最后调用代理实例的方法,完成对目标方法的横切。

    运行结果正确!

    Before
    Hello! Jack
    After

    Smart AOP 也是采用了以上思路进行了改造,如果您有兴趣的话,不妨 clone 一份 Smart Framework 的源码吧,新功能都在 dev 分支上,可能目前还没有 merge 到 master 分支上。

    最后,感谢网友 黎明伟 提供的解决方案!如果没有他的帮助,恐怕我至今都没有解决这个棘手的问题,他在我们的 QQ 群中是一名青春阳光的美少男。

    如果您也想和我们一起交流,请加入此 QQ 群:120404320

    补充(2013-11-02)

    非常感谢网友 Bieber 提出的建议:使用“链式 AOP”实现事务控制。他也实现了一个 Smart Cache Plugin,非常有特色!

    现在事务控制的机制已经统一为“链式 AOP”了,下面记录一下是如何实现的。

    首先,做了一个 TransactionAspect,用于实现事务控制的切面,代码如下:

    public class TransactionAspect extends BaseAspect {
    
        private static final Logger logger = Logger.getLogger(TransactionAspect.class);
    
        private static final DBHelper dbHelper = DBHelper.getInstance();
    
        @Override
        public boolean filter(Class<?> cls, Method method, Object[] params) {
            return method.isAnnotationPresent(Transaction.class);
        }
    
        @Override
        public void before(Class<?> cls, Method method, Object[] params) throws Exception {
            // 开启事务
            dbHelper.beginTransaction();
        }
    
        @Override
        public void after(Class<?> cls, Method method, Object[] params, Object result) throws Exception {
            // 提交事务
            dbHelper.commitTransaction();
        }
    
        @Override
        public void error(Class<?> cls, Method method, Object[] params, Exception e) {
            // 回滚事务
            dbHelper.rollbackTransaction();
        }
    }
    

    注意,这里有个 filter 方法,仅用于过滤出带有 Transaction 注解的目标方法进行事务控制。在目标方法执行前(before 方法)开启事务,在目标方法执行后(after 方法)提交事务,在遇到了异常时(error 方法)回滚事务。

    然后,在 AOPHelper 中修改如下代码:

    public class AOPHelper {
    
        ...
    
        private Map<Class<?>, List<Class<?>>> createAspectMap() throws Exception {
            // 定义 Aspect Map
            Map<Class<?>, List<Class<?>>> aspectMap = new LinkedHashMap<Class<?>, List<Class<?>>>();
            // 获取切面类
            List<Class<?>> aspectClassList = ClassHelper.getInstance().getClassListBySuper(BaseAspect.class);
            // 排序切面类
            sortAspectClassList(aspectClassList);
            // 遍历切面类
            for (Class<?> aspectClass : aspectClassList) {
                // 判断 @Aspect 注解是否存在
                if (aspectClass.isAnnotationPresent(Aspect.class)) {
                    // 获取 @Aspect 注解
                    Aspect aspect = aspectClass.getAnnotation(Aspect.class);
                    // 创建目标类列表
                    List<Class<?>> targetClassList = createTargetClassList(aspect);
                    // 初始化 Aspect Map
                    aspectMap.put(aspectClass, targetClassList);
                }
            }
            // 添加事务控制切面(最后一个切面)
            addTransactionAspect(aspectMap);
            // 返回 Aspect Map
            return aspectMap;
        }
    
        private void addTransactionAspect(Map<Class<?>, List<Class<?>>> aspectMap) {
            // 使用 TransactionAspect 横切所有 Service 类
            List<Class<?>> serviceClassList = ClassHelper.getInstance().getClassListBySuper(BaseService.class);
            aspectMap.put(TransactionAspect.class, serviceClassList);
        }
    }
    

    见以上代码片段中,创建 Aspect Map(用于存放切面类与目标类列表的映射关系)时,在末尾添加了一个事务控制切面类(TransactionAspect),让这个类横切所有继承了 BaseService 的类。

    最后,删除 TransactionProxy 与 ServiceHelper 这两个类。

    现在就可以将事务控制通过统一的 AOP 方式来实现了,而且 AOP 控制能力与范围更加强大!

    需要补充说明的是,在 ContainerListener 中,需要注意初始化 Helper 类的顺序:

    @WebListener
    public class ContainerListener implements ServletContextListener {
    
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            // 初始化 Helper 类
            initHelperClass();
            // 添加 Servlet 映射
            addServletMapping(sce.getServletContext());
        }
    
        private void initHelperClass() {
            DBHelper.getInstance().init();
            EntityHelper.getInstance().init();
            ActionHelper.getInstance().init();
            BeanHelper.getInstance().init();
            AOPHelper.getInstance().init();
            IOCHelper.getInstance().init();
        }
    
        ...
    }
    

    一定要先加载 AOPHelper,后加载 IOCHelper,大家可以思考一下这是为什么?欢迎评论。

    补充(2013-11-09)

    网友 V神 建议将 ProxyManager 改为单例模式,这样在 AOPHelper 中创建代理实例的性能会高一些。非常感谢他的建议!现已做优化:

    public class ProxyManager {
    
        private static ProxyManager instance = null;
    
        private ProxyManager() {
        }
    
        public static ProxyManager getInstance() {
            if (instance == null) {
                instance = new ProxyManager();
            }
            return instance;
        }
    
        @SuppressWarnings("unchecked")
        public <T> T createProxy(final Class<?> targetClass, final List<Proxy> proxyList) {
            return (T) Enhancer.create(targetClass, new MethodInterceptor() {
                @Override
                public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    ProxyChain proxyChain = new ProxyChain(targetClass, target, method, args, proxy, proxyList);
                    return proxyChain.doProxyChain();
                }
            });
        }
    }
    

    AOPHelper 代码片段:

    public class AOPHelper {
    
        ...
    
        private AOPHelper() {
            try {
                // 创建 Aspect Map(用于存放切面类与目标类列表的映射关系)
                Map<Class<?>, List<Class<?>>> aspectMap = createAspectMap();
                // 创建 Target Map(用于存放目标类与代理类列表 的映射关系)
                Map<Class<?>, List<Proxy>> targetMap = createTargetMap(aspectMap);
                // 遍历 Target Map
                for (Map.Entry<Class<?>, List<Proxy>> targetEntry : targetMap.entrySet()) {
                    // 分别获取 map 中的 key 与 value
                    Class<?> targetClass = targetEntry.getKey();
                    List<Proxy> baseAspectList = targetEntry.getValue();
                    // 创建代理实例
                    Object proxyInstance = ProxyManager.getInstance().createProxy(targetClass, baseAspectList);
                    // 获取目标实例(从 IOC 容器中获取)
                    Object targetInstance = BeanHelper.getInstance().getBean(targetClass);
                    // 复制目标实例中的成员变量到代理实例中
                    ObjectUtil.copyFields(targetInstance, proxyInstance);
                    // 用代理实例覆盖目标实例(放入 IOC 容器中)
                    BeanHelper.getInstance().getBeanMap().put(targetClass, proxyInstance);
                }
            } catch (Exception e) {
                logger.error("初始化 AOPHelper 出错!", e);
            }
        }
    
        ...
    }
    

    欢迎大家对 Smart Framework 的进行 code review,非常感谢!

  • 相关阅读:
    FastStone Capture(FSCapture) 注册码
    Qt下开发及调用带界面的DLL
    Gin生成证书开启HTTPS
    Gin+Vue3开启nginx gzip但是不生效。
    GIn+Docker+docer-compose
    Go字符串切片
    Vue使用AG Grid嵌套element-plus
    GIN转换UTC时间
    GORM对实现datetime和date类型时间
    (二)PaddleOCR 编译 ocr_system.dll
  • 原文地址:https://www.cnblogs.com/XJJD/p/7536827.html
Copyright © 2020-2023  润新知