使用"链式代理"实现 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 多行),总结一下都做了什么吧:
- 定义了一个 List<Proxy> proxyList,用于封装所有的 Proxy,它就是 ProxyChain 的数据载体。
- 定义了一个 int currentProxyIndex,相当于 proxyList 的当前指针,后面可以看到,它是可以往后走动的。
- 定义了一组目标类与目标方法相关的数据,其实这些数据都是来自于 CGLib 框架,后面可以看到。
- 提供了一个构造器,实现以上定义的成员变量的初始化工作。
- 提供了一组 getter 方法,用于获取目标类与目标方法的成员变量。
- 提供了一个 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,非常感谢!