Spring AOP

Spring框架的两大理念之一,AOP全称【Aspect Oriented Programming】意为面向切面编程。


AOP全称【Aspect Oriented Programming】意为面向切面编程,通过预编译和运行期间通过动态代理来实现程序功能统一维护的技术。AOP思想是OOP【面向对象】的延续,在OOP中以类【class】作为基本单元, 而 AOP中的基本单元是 Aspect【切面】,AOP是软件行业的热点,也是Spring框架中的一个重要内容。

什么是AOP

在大多数的业务中都需要验证用户是否登录才可以访问接口内容,如果没有登录或没有权限就会做出对应提示,比如下方的添加用户和修改用户的功能:

添加用户流程

修改用户流程

从以上流程可以看出,判断权限和记录日志在方法中都需要调用,如果每个流程中都加入判断权限和记录日志的代码有两个弊端:

  • 每个业务代码中都需要调用相同的权限判断功能比较重复,比较冗余
  • 业务代码中调用非业务功能,增加耦合污染业务流程

由此AOP思想就出现了,理想的架构就是将权限判断记录日志这些共用功能抽离到一个切片中,等到需要时再织入对象中去,从而改变其原有的行为。

AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。从技术上来说,AOP基本上是通过代理模式实现。

AOP体系

AOP术语

通知/增强【advice】

就是增强的功能,比如我们的程序中需要添加日志,事务等功能,我们当然需要将代码写好,封装到中,我们写好的这个方法就是通知,也可以称为增强,通知分为前置通知,后置通知,环绕通知,异常通知等,说明加在连接点的什么位置,什么时候调用

连接点【Join Point】

可以增强的功能,比如添加、修改、删除等方法都可以被增强。

切点【Pointcut】

实际上增强的方法,如上实际增强添加、删除功能,这个添加、删除就是切点。也称为切入点。需要使用表达式配置

切面 【Aspect】

切点、增强所在的那个类叫切面,这些代码需要编写出来,这个配置的类就是切面

引入【introduction】

允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗

目标对象【Target】

引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

代理【proxy】

AOP通过代理模式实现增强,会创建出来一个代理对象,融合了目标对象和增强,执行时使用的是这个新的代理对象。在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象

织入【weaving】

切面应用到目标对象来创建新的代理对象的过程。根据不同的实现技术, AOP织入有三种方式:

  • 编译器织入:这要求有特殊的Java编译器
  • 类装载期织入:这需要有特殊的类装载器
  • 动态代理织入:在运行期为目标类添加增强生成代理类的方式

Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入

实现

以用户模块为例,有添加、修改、删除、查询方法,对其中方法按照一定规则增强

基本实现

pom依赖

Spring中要使用AOP需要引入aspectjweaver依赖,如果是SpringBoot引入spring-boot-starter-aop依赖,就已经包含了aspectjweaver。

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

接口和实现类

 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
// 接口
public interface IUserService {

    List<String> list();
    int add();
    int upodate();
    int delete();
}

// 实现类
@Service
@Slf4j
public class UserServiceImpl implements IUserService {

    @Override
    public List<String> list() {
        log.info("查询用户列表");
        return Arrays.asList("A","B","C");
    }

    @Override
    public int add() {
        log.info("添加用户");
        return 0;
    }

    @Override
    public int upodate() {
        log.info("修改用户");
        return 0;
    }

    @Override
    public int delete() {
        log.info("删除用户");
        return 0;
    }
}

增强

先提供一个权限校验增强和一个日志记录增强,会使用一下几个注解:

  • @Aspect:标明类是一个切面类,里边写增强的方法和配置切入点
  • @Before:前置通知,执行方法前执行
  • @After:后置通知,执行方法后,返回退出前执行
  • @AfterReturning:后置增强,方法正常退出时执行
  • @AfterThrowing:异常抛出增强,抛出异常时执行
  • @Around:环绕增强,方法前后都执行

权限校验:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// @Aspect表示这个类是一个切面类
@Aspect
@Component
@Slf4j
public class PremissionAdvice {

    /**
     * 权限校验:需要在进入业务之前运行,使用前置通知
     */
    @Before("execution(* com.stt.service.IUserService.add()) || " +
            "execution(* com.stt.service.IUserService.upodate()) || " +
            "execution(* com.stt.service.IUserService.delete())")
    public void checkPremission() {
        log.info("权限校验......");
    }
}

日志记录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Aspect
@Component
@Slf4j
public class LogAdvice {

    /**
     * 添加日志,日志在执行完业务之后添加,使用后置通知
     */
    @After("execution(* com.stt.service.IUserService.*(..))")
    public void insertLog() {
        log.info("添加日志......");
    }
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@SpringBootApplication
public class AOPApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AOPApplication.class);

        // 获取Bean
        IUserService userService = context.getBean(IUserService.class);
        // 调用方法
        userService.add();
        userService.upodate();
        userService.delete();
        userService.list();
    }
}

环绕通知

环绕通知会在方法执行前后分别调用,比如要计算一个方法执行耗时就可以使用环绕通知

 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
@Aspect
@Component
@Slf4j
public class ExecuteTimeAdvice {

    /**
     * 获取执行时间,在执行之前
     * @return
     */
    @Around("execution(* com.stt.service.IUserService.*(..))")
    public Object getTimelong(ProceedingJoinPoint joinPoint) {
        Object result = null;
        long startTime = System.currentTimeMillis();
        // 前置业务代码
        log.info("执行前时间:{}",startTime);
        try {
            // 执行目标方法
            result = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        // 后置业务执行代码
        long endTime = System.currentTimeMillis();
        log.info("执行耗时:{}毫秒",endTime - startTime);
        return result;
    }
}

切点表达式

上边写的**execution(* com.stt.service.IUserService.*(..))**就是切点表达式,AspectJ ⽀持三种通配符:

  • ***** :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
  • .. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
  • + :表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.stt.service.IUserService+,表示继承该类的所有子类包括本身

切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:

execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)

  • 修饰符:一般省略

    • public:公共方法
    • *:任意方法
  • 返回类型:

    • void:无返回类型
    • 数据类型,如String、int等等
    • *:任意类型
  • 包:

    • com.stt.service:固定包
    • com.stt.*.service:com.stt包下任意包中的service子包
    • com.stt..:com.stt下的所有子包,包括自己
  • 类:

    • IUserService:指定类
    • *Impl:以Impl结尾的类
    • User*:以User开头的类
    • *:任意
  • 方法名:不可省略

    • addUser:固定方法
    • add*:以add开头的方法
    • *User:以User结尾的方法
    • *:任意方法
  • 参数:

    • ():无参
    • (数据类型):指定一个数据类型
    • (数据类型,数据类型):指定两个数据类型,其他以此类推
    • (..):任意参数
  • throws:异常类型,一般省略不写

表达式示例

  • execution(* com.stt.demo.User.*(..)) :匹配 User 类⾥的所有⽅法。
  • execution(* com.stt.demo.User+.*(..)) :匹配该类的⼦类包括该类的所有⽅法。
  • execution(* com.stt..(..)) :匹配 com.stt 包下的所有类的所有⽅法。
  • execution(* com.stt...(..)) :匹配 com.stt 包下、⼦孙包下所有类的所有⽅法。
  • execution(* addUser(String, int)) :匹配 addUser ⽅法,且第⼀个参数类型是 String,第⼆个参数类型是 int。

其他切点表达式

  • arg():限定连接点方法参数
  • @args():通过连接点方法参数上的注解进行限定
  • execution():用于匹配是连接点的执行方法
  • this() :限制连接点匹配 OP Bean 用为指定的类型
  • target:目标对象(即被代理对象)
  • @target():限制目标对象的配置了指定的注解
  • within:限制连接点匹配指定的类型
  • @within():限定连接点带有匹配注解类型
  • @annotation():限定带有指定注解的连接点

当然表达式之间可以通过**||**【或者】或者 **&&【并且】**连接,所有的表达式可通过官网查看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 匹配指定包中的所有的方法, 但不包括子包
within(com.stt.service.*)
 
// 匹配指定包中的所有的方法, 包括子包
within(com.stt.service..*)
 
// 匹配当前包中的指定类中的方法
within(UserService)
 
// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)
 
// 匹配以指定名字结尾的 Bean 中的所有方法
bean(*Service)
 
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)
 
// 匹配名字以 Service 开头, 并且在包 com.stt.service 中的 bean
bean(*Service) && within(com.stt.service.*)

单独定义切点

切点支持单独配置,之后引用到需要该切点的增强

1
2
3
4
5
6
7
8
9
// 定义切点
@Pointcut("execution(* com.stt.service.IUserService.*(..))")
public void afterLog(){}

// 使用切点到增强上
@After("afterLog()")
public void insertLog() {
    log.info("添加日志......");
}

Spring AOP原理

AOP的实现方式其实是代理模式,代理模式是给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用,代理模式分为静态代理动态代理

  • 静态代理:是由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行之前,代理类.class文件就已经被创建了。
  • 动态代理:在程序运行时通过反射机制动态创建。

Spring AOP的原理是构建在动态代理基础上,Spring对AOP的支持局限于方法级别。Spring AOP 支持 JDK ProxyCGLIB 方式实现动态代理。

  • 默认情况下,实现了接口的类,使用 AOP 会基于 JDK ⽣成代理类;
  • 没有实现接⼝的类,会基于 CGLIB ⽣成代理类。

静态代理

接口

1
2
3
public interface IStaticProxyService {
    void staticProxyMethod();
}

实现类

1
2
3
4
5
6
7
8
@Slf4j
@Service
public class StaticProxyServiceImpl implements IStaticProxyService {
    @Override
    public void staticProxyMethod() {
        log.info("静态代理对象原对象方法......");
    }
}

代理类:代理类将实现类依赖进来,并实现接口,重写方法在实现自己逻辑的同时调用实现类方法以此实现代理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Service
@Slf4j
public class StaticProxyServiceProxy implements IStaticProxyService {
    private final IStaticProxyService staticProxyServiceImpl;

    public StaticProxyServiceProxy(IStaticProxyService staticProxyServiceImpl) {
        this.staticProxyServiceImpl = staticProxyServiceImpl;
    }

    @Override
    public void staticProxyMethod() {
        log.info("静态代理对象方法......");
        // 调用原对象方法
        staticProxyServiceImpl.staticProxyMethod();
    }
}

小结:

  • 优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
  • 缺点:需要为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。

JDK动态代理

不需要生成实现接口,使用JDK的API在内存中构建代理对象

接口

1
2
3
public interface IJDKProxyService {
    void jdkProxyMethod();
}

实现类

1
2
3
4
5
6
7
@Slf4j
public class JDKProxyServiceImpl implements IJDKProxyService {
    @Override
    public void jdkProxyMethod() {
        log.info("JDK动态代理接口方法......");
    }
}

代理类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Slf4j
@Service
public class JDKProxyServiceProxy implements InvocationHandler {
    private final Object target;

    public JDKProxyServiceProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        log.info("JDK动态代理......");
        // 调用被代理对象方法
        result = method.invoke(target,args);
        return result;
    }
}

测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//被代理对象
JDKProxyServiceImpl jdkProxyService = new JDKProxyServiceImpl();
//代理对象
JDKProxyServiceProxy serviceProxy = new JDKProxyServiceProxy(jdkProxyService);
//获取类加载器
ClassLoader classLoader = jdkProxyService.getClass().getClassLoader();
//获取该类的接口
Class<?>[] interfaces = jdkProxyService.getClass().getInterfaces();
IJDKProxyService ijdkProxyService = (IJDKProxyService)Proxy.newProxyInstance(classLoader, interfaces, serviceProxy);
ijdkProxyService.jdkProxyMethod();

小结

  • 实现InvocationHandler接口重写invoke方法
  • 创建代理对象使用Proxy类的newProxyInstance方法,传入实现类类加载器,接口和代理类的对象

CGLB动态代理

接口

1
2
3
public interface ICGLIBProxyService {
    void cglIbMethod();
}

实现类

1
2
3
4
5
6
7
@Slf4j
public class CGLIBProxyServiceImpl implements ICGLIBProxyService {
    @Override
    public void cglIbMethod() {
      log.info("CGLIB代理实现类方法......");
    }
}

代理类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Slf4j
public class CGLIBProxyInterceptor implements MethodInterceptor {

    //被代理对象
    private Object target;

    public CGLIBProxyInterceptor(Object target){
        this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        log.info("CGLIB代理方法......");
        //通过cglib的代理⽅法调⽤
        Object retVal = proxy.invoke(target, args);
        return retVal;
    }
}

测试

1
2
3
4
5
6
7
// 创建实现类对象
ICGLIBProxyService target= new CGLIBProxyServiceImpl();
// 构建代理对象
ICGLIBProxyService proxy= (ICGLIBProxyService) 
                           Enhancer.create(target.getClass(),
                           new CGLIBProxyInterceptor(target));
proxy.cglIbMethod();

JDK 和 CGLIB 实现的区别

  • JDK实现:要求被代理类必须实现接⼝,之后是通过 InvocationHandler 及 Proxy,在运⾏时动态的在内存中⽣成了代理类对象,该代理对象是通过实现同样的接⼝实现(类似静态代理接⼝实现的⽅式),只是该代理类是在运⾏期时,动态的织⼊统⼀的业务逻辑字节码来完成。
  • CGLIB实现:被代理类可以不实现接⼝,是通过继承被代理类,在运⾏时动态的⽣成代理类对象 (三方框架,一般性能有优势)。

–来自B站[石添的编程哲学]