本文是《轻量级 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 多行),总结一下都做了什么吧:
先判断当前指针有没有走完所有的 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,非常感谢!