JAVA AOP理论与实践
之前的博客写过关于JAVA反射、注解的相关理论和实践,另外一个比较相关又晦涩的东西就是AOP切面了。
目前我对JAVA的学习思路,主要是优先掌握这些最晦涩的部分,因为语言的其他部分无非就是基础语法和类库,基于此前我个人的开发经验来说都不是什么问题。
这个学习的动力,主要来源于对Java web框架的各种高大上设计的好奇心,这在其他语言WEB框架设计里虽然能找到一些模仿的影子,但毕竟还是不够透彻。
废话不多说,项目地址:https://github.com/owenliang/java-aop。
切面理论
用途
就是给原有的方法外面加一层包装,如果用过python的装饰器语法就知道了,完全就是一码事,换了个叫法。
好处当然就是不用改原有的方法代码,对于JAVA来说可以做到原有代码一行不改,文件一行不变。
切面Aspect
JAVA在实现这个能力的时候,首先需要你实现自己的包装器类,也就是一个切面,英文Aspect。
Aspect通常可以在原有方法执行前、执行后两个位置被回调,这个回调的位置就叫做”joinPoint连接点”,一般就是before和after两种。
目标Target
切面需要连接到1个或者N个方法,这些方法所处的对象叫做Target。
最终的效果是,当target对象的某些方法被调用时,切面对象可以被回调,从而实现”切入”的效果。
连接点JoinPoint
因为1个切面对象,可以被连接到不同类的不同方法,被大家共享使用。
所以当切面对象被回调的时候,需要告诉切面对象当前是哪个类对象的哪个方法被调用,这样切面对象可以获知这些信息,我称这些调用点的信息为JoinPoint。
代理Proxy
JAVA是一个强类型语言,切面的目标是包装Target的方法,从而可以在方法调用前后实现切面逻辑,但是切面又不需要我们改Target的方法实现,这里面是有什么黑科技么?
JAVA采用了一个朴素的思路,就是创造一个子类,让它继承Target类,这样子类覆写Target类的所有方法,在其中就可以引入切面回调:
1 2 3 4 5 6 |
void aMethod() { // do something before Parent.aMethod(); // do something after { |
JAVA通过库Cglib,可以实现在运行时根据指定的class动态生成一个子class,这个子class称为一个Proxy。
库Cglib允许我们注册一个回调函数到Proxy对象,每当Proxy的方法被调用就会委托给回调函数处理,我们可以在回调函数里实现切面的回调。
实践
下面会按上述理论概念,逐段呈现相关代码实现,因为原理都比较清晰,对照代码去理解即可。
流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package cc.yuerblog; public class App { public static void main( String[] args ) { // 1, 目标的代理对象(任何被切的目标对象都应该分配一个代理对象,从而实施切面) AspectProxy proxy = new AspectProxy(); // 2, 创建一个切面 AspectInterface aspect = new Aspect(); // 3, 为目标的某些方法注册切面(这一步应该反射Aspect.java中注解,识别出切面要注入给哪些类) proxy.addAspect("aMethod", aspect); proxy.addAspect("bMethod", aspect); // 4, 生成被代理的目标对象 AspectTarget target = proxy.buildTargetProxy(AspectTarget.class); // 5, 调用目标的方法,对应切面得到回调 target.aMethod(); target.bMethod(); target.cMethod(); // 步骤1,2,3均可以基于注解+运行时反射自动实现, 这里就不演示了 } } |
AspectTarget类是目标对象,我无需对它做任何代码修改,就可以将Aspect切面连接过去,从而实现在Target调用前后打印日志的效果。
因为Target需要连接切面,所以Target对象需要一个Proxy,Proxy对象是通过Cglib来动态生成的,AspectProxy是一个类似工厂的类。
切面Aspect抽象
切面连接到目标,同时切面的逻辑是业务自定义的,所以需要对切面做抽象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package cc.yuerblog; // 切面 interface AspectInterface { public void before(JoinPoint joinPoint); public void after(JoinPoint joinPoint); } // 这里可以基于注解声明切面要连接到哪些目标, 例如(下面是伪代码): // @execution(class="cc.yuerblog.AspectTarget", method={"aMethod","bMethod"}) // 该注解也称为:PointCut切点,意思是切入哪些bean的哪些方法 public class Aspect implements AspectInterface { public void before(JoinPoint joinPoint) { System.out.println("before: " + joinPoint.object.getClass().getSuperclass().getName()+ "." + joinPoint.method.getName()); } public void after(JoinPoint joinPoint) { System.out.println("after: " + joinPoint.object.getClass().getSuperclass().getName() + "." + joinPoint.method.getName()); } } |
一个切面接口有2个回调,分别在方法调用前和方法调用后,这是我对切面的一个基本抽象。
我实现了一个切面,在回调方法中打印出了连接点的信息,也就是当前是哪个方法调用引起的切面回调。
连接点JoinPoint
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package cc.yuerblog; import java.lang.reflect.Method; // JoinPoint连接点 // 主要用于告知切面对象,当前是哪个对象的哪个方法被调用 public class JoinPoint { JoinPoint(Object obj, Method method) { this.object = obj; this.method = method; } public Method method; public Object object; } |
告知切面哪个方法被调用,这些都是基于运行时反射获得的信息,在下面会看到。
目标Target
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package cc.yuerblog; public class AspectTarget { // 被切入 public String aMethod() { System.out.println("aMethod"); return "aMethod"; } // 被切入 public String bMethod() { System.out.println("bMethod"); return "bMethod"; } // 未被切入 public String cMethod() { System.out.println("cMethod"); return "cMethod"; } } |
既原有的类(被代理的类),只需将上面的切面连接到该类的aMethod和bMethod方法,而不需要对AspectTarget类进行任何修改。
代理 Proxy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
package cc.yuerblog; import org.mockito.cglib.proxy.Enhancer; import org.mockito.cglib.proxy.MethodInterceptor; import org.mockito.cglib.proxy.MethodProxy; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class AspectProxy implements MethodInterceptor { // 注册的切面对象 private Map<String, ArrayList<AspectInterface>> aspectMap; AspectProxy() { aspectMap = new HashMap<String, ArrayList<AspectInterface>>(); } /** * 创建目标的代理对象 * @param superCls * @param <T> * @return */ public <T> T buildTargetProxy(Class<T> superCls) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(superCls); enhancer.setCallback(this); return (T)enhancer.create(); } /** * 注册一个切面到代理对象 * @param method * @param aspect */ public void addAspect(String method, AspectInterface aspect) { if (!aspectMap.containsKey(method)) { ArrayList list = new ArrayList(); list.add(aspect); aspectMap.put(method, list); } else { aspectMap.get(method).add(aspect); } } /** * 代理对象接管目标的方法调用, 实施切面 * @param o * @param method * @param objects * @param methodProxy * @return * @throws Throwable */ public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { // joinPoint连接点: before ArrayList<AspectInterface> aspectArr = aspectMap.get(method.getName()); if (aspectArr != null) { for (AspectInterface aspect : aspectArr) { aspect.before(new JoinPoint(o, method)); } } // 调用目标的方法 Object ret = methodProxy.invokeSuper(o, objects); // joinPoint连接点: after if (aspectArr != null) { for (AspectInterface aspect : aspectArr) { aspect.after(new JoinPoint(o, method)); } } return ret; } } |
该类用于生成Target的代理对象,通过buildTargetProxy函数获得生成代理类对象。
Enhancer是Cglib库的类,需要告诉它继承的基类(也就是target),再需要设置一个委托对象,每当proxy的方法被调用就会回调该对象的intercept方法。
在这里,我的AspectProxy对象自身就实现了intercept方法,所以enhancer.setCallback(this)即可。
既然enhancer的callback处理器就是AspectProxy自身,那么就需要在AspectProxy.intercept方法中实现切面的回调逻辑。
至于哪些切面要被回调,这需要我们主动向AspectProxy注册切面,由aspectMap维护这个关系。
当intercept回调时,method是AspectTarget对应被调用的方法反射,objects是调用参数,o就是enhancer生成的代理对象,methodProxy是代理对象被调用的方法反射,这几个比较绕,但是不必太在意。
我要做的就是,在真正调用父类(AspectTarget)的方法之前回调所有注册切面的before方法(同时传入连接点信息),在之后调用after方法,仅此而已。
效果
1 2 3 4 5 6 7 |
before: cc.yuerblog.AspectTarget.aMethod aMethod after: cc.yuerblog.AspectTarget.aMethod before: cc.yuerblog.AspectTarget.bMethod bMethod after: cc.yuerblog.AspectTarget.bMethod cMethod |
可见,Target的aMethod和bMethod都被同一个切面连接,调用时打印了日志。
而cMethod因为没有注册切面,所以没有打印日志。
参考文章
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

👍
COM里面说了多少年了, 面向interface编程, 抛弃OOP的概念! 这就是AOP
👍