Spring核心特性——AOP详解
前言
我们曾经在谈到Spring 的Transactional 注解时提到了AOP,并言明了AOP是该注解实现的基础。
但是说到底,还没有系统的介绍过AOP,讲Spring不提AOP总归是缺了点什么的。而且,相信大家在面试的时候也经历过不少AOP相关的提问,例如
- 什么是SpringAop?
- Spring通知有哪些类型?
- 动态代理的两种方式?
本次我们就好好来讲讲SpringAop
一、SpringAop是什么?
在IT行业里,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。
打个比方,我们现在有一个老项目,因为早期项目的不规范,输出的日志很少,导致定位BUG很困难。所以想对业务方法能以log形式打印出入参、出参。但是我们不可能真的每个方法都去改代码。
这时候就可以用到AOP,,我们可以先写一段打印日志的代码,然后把业务方法作为切入点,把打印日志的代码作为增强模块植入进去。这样,在每次访问业务方法前和后,就会执行打印日志的代码,输出入参及出参
二、简单使用
1. 术语介绍
在接触SpringAop之前,我们需要知道一些术语:
-
连接点(Joinpoint)
能够被增强的方法都算连接点 -
切入点(Pointcut)
实际需要被增强的方法的集合 -
增强/通知(Advice)
实际增强的那部分代码称为增强 -
切面(Aspect)
增强和切入点的结合
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AopTest {
// 通知(增强):本方法的内容
// 切入点:com.zhanfu.service包下的所有方法
// 增强种类:前置增强(before)
// 切面:本类包含通知及切入点,本类就是切面类
// 本方法的含义就是,在执行com.zhanfu.service包的所有方法前,都先执行本方法
@Before("execution(* com.zhanfu.service..*.*(..))")
public void before(JoinPoint joinPoint){
System.out.println("前置通知:在目标执行前被调用的通知");
}
}
2. 增强的种类
我们前面说了增强其实就是在执行目标方法时,多执行一点”增强“的内容,那么根据”增强“内容 与 原方法的代码顺序或代码顺序,我们很自然的把增强分为几种类型。
-
Before 前置增强
增强方法先于目标方法执行。在核心功能之前执行的额外功能 -
After 后置增强
增强方法在目标方法执行后执行,无论目标方法运行期间是否出现异常。在核心功能之后执行的额外功能 -
AfterReturning 返回增强
在目标方法执行后,返回执行结果时执行,当目标函数抛出异常时它不会执行 -
AfterThrowing 异常增强
当目标函数抛出异常时,异常增强会执行 -
Around 环绕增强
环绕增强中可以实现上述四种增强 -
DeclareParents 引入增强
这种增强与上面不同,它不是在某个方法前执行代码,而是为被增强的类,增加一个接口,并提供这个接口的实现方法,我们可以理解成它为目标类增加了方法
3. 常见用法
使用AOP最常见的用法,就是定义个@Aspect注解类,在该类里面写上大量的切入点和增强方法,当然也可以以一种组合的方式,直接把pointcut的定义写在增强方法注解上。
@Aspect
@Component
public class MyAspect {
/**
* 以注解方式找到切入点
* 此处是找到被 MyAnnotation 注解标记的 methodA()方法,把这些方法作为切入点
*/
@Pointcut("@annotation(com.zhanfu.springtest.MyAnnotation)")
public void someService1(){}
/**
* 在指定的范围找切入点
* 此处是 找到定义在springtest包里的任意方法
*/
@Pointcut("execution(* com.zhanfu.springtest.*.*(..)) ")
public void someService2(){}
/**
* 增强方法,本增强方法作用于上面定义的切入点someService1(),
* 增强方式为Around
*/
@Around("someService1()")
public Object doSomething (ProceedingJoinPoint joinPoint){
System.out.println("执行前");
try {
Object result = joinPoint.proceed();
System.out.println("执行后");
return result;
} catch (Throwable e) {
System.out.println("发生异常");
e.printStackTrace();
}
return null;
}
/**
* 增强方法,组合使用,这种方式不需要另外写个pointcut,而是直接把 pointcut 写在@before注解里面
* 增强方式为Before
*/
@Before("execution(* com.zhanfu.springtest.*.main(..))")
public void before(ProceedingJoinPoint joinPoint){
String name = joinPoint.getSignature().getName();
System.out.println("前置通知:在" + name + "执行前");
}
}
4. Spring内置的增强
除了上述供开发者去创建的增强外,Spring其实框架本身也内置了一些增强样例,使得我们仅只用注解就可以享受到一些特定功能的增强,从而大大减少程序员的工作量,比如事务相关的 @Transctional 或者 缓存相关的 @Cacheable
我们在项目中开启相关功能后,使用上述注解,比如 @Transctional,就可以不必重复写获取数据库连接,会话,以及各种设置等代码,Spring会根据配置自动完成这些内容。具体内容,可以看我的另一篇博文 Spring事务畅谈 —— 由浅入深彻底弄懂 @Transactional注解
5. SpringAop 与动态代理
我们都知道,SpringAop 是基于动态代理实现的。但是两者并不是等号关系,Aop是一种技术手段和思想,它的实现会用到很多其他技术,其中就包含了动态代理技术。但动态代理并不是全部,事实上,生成动态代理仅需要几行代码,但在这之前的准备工作却是巨大的,这部分工作都由Spring完成了。这让我们可以使用简单的注解或配置,实现复杂场景下的动态代理
而动态代理的创建,在Spring里用到了两种:
-
jdk动态代理:
原对象需要有实现接口,生成的代理,只包含接口里定义的方法 -
CGlib动态代理:
生成原对象的继承对象,并重写同名方法
6. Aop功能的易错点(重点)
我们在使用Aop的时候,实际是在利用动态代理和原对象打交道,中间多了一层,导致存在大量失效场景,这是我们在使用SpringAop时需要格外注意的:
-
接口和修饰符问题
采用Cglib时,final 修饰的、static 修饰的 、private 修饰的方法无法代理,因为这些方法无法继承。同样的采用jdk动态代理时,需要原对象有接口,且只能代理接口里定义的方法 -
同一个类中的方法调用
同一个类里的两个方法,它们之间相互调用时不会触发增强内容,如下图,方法B的增强并不会被执行到,因为同一个对象里,方法A调用方法B,用的是this.B()。即执行原对象的方法A时,发现要用方法B,就会直接调用本对象的方法B,而不会再绕回去调用代理对象的方法B
-
复杂项目场景
- 一些复杂项目可能会有多个BeanFactory,即Bean容器,这些容器之间的Bean不通用,可能会导致没有产生代理
- 过早创建的Bean,有一些Bean由于不合理的设定或者循环引用,导致过早(在BeanPostProcessor生效前)被创建并放入容器,那么这些Bean就没有创建代理,需找到对应位置,使用@lazy 延缓其Bean创建时间
三、手写动态代理
1. 创建动态代理
我们如果想了解AOP,不如自己动手做一遍,这里我们只看最核心的部分,有兴趣的可以复制我的代码直接测试,我们这里利用的是 jdk动态代理:
首先,我们定义一个接口:吃
public interface EatInterface {
void eat();
}
然后为接口写个实现类:人
public class Human implements EatInterface {
@Override
public void eat() {
System.out.println("我是战斧 , 我吃东西");
}
public void cook() {
System.out.println("我是战斧 , 我做饭");
}
}
然后再写个增强类,这个类必须实现 InvocationHandler
public class WashHandler implements InvocationHandler {
private Object target;
public WashHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("做任何事前都要洗手");
Object retVal = method.invoke(target, args);
System.out.println("做任何事后都要洗手");
return retVal;
}
}
最后来个类,执行其main方法
public class MainClass {
public static void main(String[] args) {
// 设置系统属性,使得生成的动态代理类可以被保存下来
System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
Human human = new Human();
WashHandler handler = new WashHandler(human);
EatInterface proxyRes = (EatInterface) Proxy.newProxyInstance(human.getClass().getClassLoader(),
human.getClass().getInterfaces(), handler);
proxyRes.eat();
}
}
执行的结果是符合预期的
就这样,我们生成了个动态代理,并且完成了一次增强。
2. 查看代理类
我们或许好奇是如何实现的,其实这是使用的JDK的动态代理,那我们就可以看看这个动态代理类的内容是什么,我们先前加的代码
System.getProperties().setProperty(“sun.misc.ProxyGenerator.saveGeneratedFiles”, “true”);
就起了作用,我们可以在项目源码根目录找到生成的 $Proxy0
我们打开该类,查看其代码
package com.sun.proxy;
import com.zhanfu.service.EatInterface;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements EatInterface {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void eat() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.zhanfu.service.EatInterface").getMethod("eat");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
不难发现几个有意思的点:
- 创建的动态代理类继承了 Proxy 类,同时实现了我们传给它的接口 EatInterface
- 代理类包含接口的方法 ,且补全了几个Object的方法,即hashCode()、toString()、equals(),但却没有实现类自己的方法cook()
- 所有的方法都采用 super.h.invoke 进行调用,而这里的h,自然就是我们传进去的WashHandler
3. 分析调用链路
其实从上面两个小节可以感受到,动态代理的创建并不复杂。
我们首先的创建个 ”调用处理器“,它的作用是在调用我们给定的 ”某个方法“ 时,能够执行点别的代码。这也就是我们说的”增强“模块。所以它的方法 invoke 入参包含了对象的方法以及方法的入参。
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
第二步才是动态代理,以JDK代理为例,首先肯定是动态创建个类,然后将这个类实例化成对象(内部有调用处理器)。这个代理对象的方法和原对象一致,只是它的方法体非常简单,就是调用我们的 ”调用处理器“。因此不难发现,此时真正用到的还是我们的 ”调用处理器“,然后再由 ”调用处理器“ (内部有原对象)去调用原对象的方法
4.手写代理的不足
从上面的例子里,我们已经利用jdk自带方法创建了个动态代理,但是仔细想想,要在生产种使用这种方式实现Aop,它还存在以下问题,如
- 需要手动创建所有对象,每次使用都得重新创建,非常麻烦
- 当我需要对很多对象做同一种增强时,难以实现,因为要找到这些对象,还要将其存入调用处理器
- 当某一个对象需要被多种调用处理器增强时,难以实现,因为入参只允许传一个调用处理器
解决方式也比较明了:
- 设计个容器,创建的对象扔在里面,随取随用,包括动态代理对象
- 支持以各种表达式的方式进行配置,灵活的为每一个调用处理器找到所有需增强的对象
- 当多个调用处理器对同一个对象做增强时,需要有排序,然后按照排序完成嵌套增强
当然,其实看这解决方式,我们会感到非常熟悉,是的,Spring已经包含了这些功能。这才使得,我们可以通过简单配置切面,就能完成对应的增强功能了。