快速了解 AOP 必要知识并给出 demo。这篇文章是在我系统学了 AOP 的基本使用之后再写的,之前遇到的很多痛点都体现不出来了。花了挺多时间读官方文档的时候,然后很多问题都迎刃而解了,就是比较花时间。之前失败的主要原因是 pointcut 的表达式写错了,tomcat 起了,但是服务都失败了。。。。
简单概括什么是 AOP
不破坏代码结构的情况下,为代码添加功能,典型案例如打印方法的执行时间。
AOP 涉及的专有名词
- Joinpoint - 要匹配的方法
- Pointcut - 匹配方法的规则
- Advice - 检测到匹配方法的时候要执行的 额外 逻辑
- Aspect - 带 @Aspect 的那个类
详细的指代会在下面的例子中标记出来,光是名词实在难记
怎么写
如果 Spring 整体环境已经搭建完成,如果你想要新建一个 Aspect 只需要做两件事
- 写 Aspect 文件,最简单的方式是写 Advice 方法,并在注解中添加 pointcut 表达式
- 注册 Aspect, 可以通过 Xml 或者 Annotation 注册
在实际例子中标注 AOP 相关的概念,直接看名词很难记住,理解
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
| @Aspect @Component public class MyAspect { @Pointcut("target(official.AopTestService)") public void pointcutName() {} @Around("pointcutName()") public Object aroundSayHello(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("Before join point..."); Object obj = joinPoint.proceed(); System.out.println("After join point..."); return obj; } }
@Component public class AopTestService { public void sayHello(String name) { System.out.println("Hello, " + name); }
public void greet() { System.out.println("Hello, there"); } }
@Configuration @EnableAspectJAutoProxy @ComponentScan(basePackages = "official") public class AppConfig {}
public class Client { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); ctx.getBean("aopTestService", AopTestService.class).greet(); ctx.getBean("aopTestService", AopTestService.class).sayHello("Jack"); } }
|
原理剖析
所有的实现都是基于代理模式完成的。基础版本,不借助任何 lib, 纯 Java 实现。假设我们有 ISubject 接口,和对应的实现类 SubjectImpl,代理类 SubjectProxy。我们可以通过调用 SubjectProxy 来触发 SubjectImpl 并添加我们自己的定制逻辑。
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
| public interface ISubject { void request(); }
public class SubjectImpl implements ISubject { @Override public void request() { System.out.println("Invoke request in SubjectImpl..."); } }
public class SubjectProxy implements ISubject { private ISubject subject;
@Override public void request() { System.out.println("Call request in proxy..."); subject.request(); } }
public class Client { public static void main(String[] args) { ISubject subject = new SubjectProxy(new SubjectImpl()); subject.request(); } }
|
进阶使用,如果还有其他接口比如 IRequestable 也有 request 方法,那么为了实现代理,我们还需要为他新建一个代理类,如果类似的类很多,就会有很多代码冗余。JDK提供了动态代理的机制解决这种问题,概括来就是给出时间对象和目标接口,就能实现代理逻辑的统一操作
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
| public class RequestCtrlInvocationHandler implements InvocationHandler { private Object target;
public RequestCtrlInvocationHandler(Object target) { this.target = target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Invoke method in InvocationHandler..."); if (method.getName().equals("request")) { return method.invoke(target, args); } return null; } }
public class InvocationHandlerClient { public static void main(String[] args) { SubjectImpl impl = new SubjectImpl(); ISubject subject = (ISubject) Proxy.newProxyInstance( impl.getClass().getClassLoader(), new Class[]{ISubject.class}, new RequestCtrlInvocationHandler(impl)); subject.request(); } }
|
再进一步,上面的方法只能处理实现了接口的情况,如果类并没有实现接口,还想要增强的话,我们需要借助 Cglib 这个第三方库,在类上派生出一个子类,通过复写方法达到扩展的目的。
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
| public class Requestable { public void request() { System.out.println("Call in Requestable, without interface..."); } }
public class RequestCtrlCallBack implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { if (method.getName().equals("request")) { System.out.println("Invoke method in MethodInterceptor..."); return methodProxy.invokeSuper(o, objects); } return null; } }
public class CallBackClient { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Requestable.class); enhancer.setCallback(new RequestCtrlCallBack());
Requestable proxy = (Requestable) enhancer.create(); proxy.request(); } }
|
Spring 的实现中,会判断是否有接口,如果有则使用动态代理,如果没有则使用字节码增强。
奇怪的 behavior
最近发现产品上的 AOP 代码在两个 aspect 嵌套的情况下只会执行第一个 aspect,第二个直接跳过了,不知道是公司特有的还是 AOP 本来就有这种设定,特意检测一下
场景重现:
service A 有两个 method01,method02 并且 method01 会调用 method02. 创建 Aspect 同时覆盖这两个方法,当 method01 执行时,method02 并不会被检测到。
搜了一下 stackoverflow, 有人指出这个现象底层原理已经在 5.8.1 中写了。。。。proxy 之后,对自己的调用将会失效
Spring 4.3 之后可以通过 class 中注入本身来绕过这个问题
stackoverflow exp + solution