Spring AOP-AspectJ注解实现拦截


铿然架构 | 作者 / 铿然 这是 铿然架构 的第 121 篇原创文章 ***

1. 基本概念

1.1 概念

Spring AOP涉及的基本概念如下:

概念 描述
Aspect 切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。切面由切入点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了切入点的定义。 Spring AOP是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
Join point 连接点是在应用执行过程中能够插入切面的一个点,这个点可 以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。Spring当前还不支持修改字段连接点。
Advice 通知是切面要做的工作,是指拦截到连接点之后要执行的代码,通知定义了要"做什么"以及"何时做",包括"around"、"before"和"after"等不同类型的通知。
Pointcut 切入点是对连接点(被拦截的方法)进行拦截的条件定义。作用是提供一组规则来匹配连接点,给满足条件的连接点添加通知,切入点定义了在"何处做"。
Introduction 用来声明额外的方法和属性,可以给目标对象引入新的接口及其实现,在无需修改现有类的情况下,让它们具有新的行为和状态。
Target object 目标对象指将要被增强的对象,即包含主业务逻辑的类对象。
AOP Proxy AOP中会通过代理的方式,对目标对象生成一个代理对象,代理对象中会加入需要增强功能,通过代理对象来间接的方式目标对象,起到增强目标对象的效果。
Weaving 织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。织入可以在编译期,类加载期和运行期完成,在编译期进行织入就是静态代理,而在运行期进行织入则是动态代理。

以上这些概念在初次接触比较难理解,下面我们通过一张图来理解:

● Aspect切面类必须使用@Component和@AspectJ注解才有效。

● Aspect切面类可以包含多个Advice方法。

● Advice注解和Advice方法组成了Advice,它们分别定义了"何时做"和"做什么"。

● Advice注解通过Pointcut判断哪些target对象的方法要调用advice方法,即定义了在"何处做"。

● 调用target对象方法、抛出异常就是JoinPoint,可以在这些连接点插入切面,同时Advice可以使用JoinPoint。

1.2 代码示例

java 复制代码
@Component  // 必须添加此注解才会生效
@Aspect  //此注解用于标识是切面类,这个切面类由advice(拦截目标对象方法后要做增强处理的方法)和pointcut组成
public class LoggingAspect {

    // @Before是advice注解,用于定义一个advice方法,在被拦截的目标方法调用之前增加处理逻辑,定义了"何时做"
    // execution定义的pointcut,作为一个谓词判断哪些方法要被拦截,这段代码的含义是:
    // 指定包及其子包下的所有public方法都被拦截,定义了"何处做"
    // JoinPoint就是接入点了,可以获取到目标类的一些信息
    @Before(value = "execution(public * com.kengcoder.springframeawsome.aop.service..*(..))")
    public void before(JoinPoint joinPoint) {
        // Advice方法,定义了"做什么"
        log.info("before");
        log.info("before: joinPoint.getThis() " + joinPoint.getThis());
        log.info("before: joinPoint.getKind() " + joinPoint.getKind());
        log.info("before: joinPoint.getTarget() " + joinPoint.getTarget());
        log.info("before: joinPoint.getSignature() " + joinPoint.getSignature());
        log.info("before: joinPoint.getSourceLocation() " + joinPoint.getSourceLocation());
        log.info("before: joinPoint.getStaticPart() " + joinPoint.getStaticPart());
    }

    // @Around是advice注解,用于定义一个advice方法,在被拦截的目标方法调用之前和之后增加处理逻辑,定义了"何时做"
    // bean定义的pointcut,作为一个谓词判断哪些bean的方法要被拦截,这段代码的含义是:名称为"bookService"的bean的所有方法都被拦截,定义了"何处做"
    // ProceedingJoinPoint就是接入点,它是JoinPoint的子类,两者的区别是:需要手动调用ProceedingJoinPoint的proceed方法,目标方法才会得以执行,
    // 而JoinPoint仅用于获取信息,目标方法会自动被调用
    @Around(value = "bean(bookService)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // Advice方法定义了"做什么"
        log.info("before around");
        // 调用目标方法
        Object result = joinPoint.proceed();
        log.info("after around");
        return result;
    }
}

JoinPoint信息参考:

java 复制代码
joinPoint.getThis() com.kengcoder.springframeawsome.aop.service.BookService@6a84bc2a
joinPoint.getKind() method-execution
joinPoint.getTarget() com.kengcoder.springframeawsome.aop.service.BookService@6a84bc2a
joinPoint.getSignature() Book com.kengcoder.springframeawsome.aop.service.AbstractBookService.queryBookByAuthor(String)
joinPoint.getSourceLocation() org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint$SourceLocationImpl@71ad3d8a
joinPoint.getStaticPart() execution(Book com.kengcoder.springframeawsome.aop.service.AbstractBookService.queryBookByAuthor(String))

2. 启用Spring AOP

2.1 引入aspectj依赖

java 复制代码
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.7</version>
        </dependency>

2.2 引入Spring AOP依赖

java 复制代码
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.0.RELEASE</version>
        </dependency>

注:此依赖不一定需要显示引入,如果引入了"spring-boot-starter-web"依赖则会自动间接引入,不需要重复引入:

2.3 EnableAspectJAutoProxy注解

在启动类上加上此注解:

java 复制代码
@SpringBootApplication
@EnableAspectJAutoProxy
public class StartupApplication {
    public static void main(String[] args) {
        SpringApplication.run(StartupApplication.class, args);
    }
}

注:此注解也不是必须的,如果引入了"spring-boot-starter-web"依赖则会自动启用AOP:

间接引入的"spring-boot-autoconfigure"模块中使用了AOP的自动配置类"AopAutoConfiguration":

AopAutoConfiguration代码如下:

java 复制代码
@AutoConfiguration
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(Advice.class)
	static class AspectJAutoProxyingConfiguration {

		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = false)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
		static class JdkDynamicAutoProxyConfiguration {
		}

		@Configuration(proxyBeanMethods = false)
		@EnableAspectJAutoProxy(proxyTargetClass = true)
		@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
				matchIfMissing = true)
		static class CglibAutoProxyConfiguration {
		}
	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
	@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
			matchIfMissing = true)
	static class ClassProxyingConfiguration {

		@Bean
		static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
			return (beanFactory) -> {
				if (beanFactory instanceof BeanDefinitionRegistry) {
					BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
					AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
					AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
				}
			};
		}
	}
}

类最前面有一行:

java 复制代码
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)

这一行会根据配置参数spring.aop.auto决定是否启用aop,当没有配置时默认值是true,即默认启用aop。

spring.aop.auto配置参数和EnableAspectJAutoProxy注解的关系如下:

spring.aop.auto EnableAspectJAutoProxy 是否启用AOP
不配置 不使用 启用
false 不使用 不启用
false 使用 启用
true 使用 启用

即只要使用任意一种方式都会启用Spring AOP,如果统一都加上"EnableAspectJAutoProxy"不会错,当不加时也能启用不要觉得奇怪。

3. Advice注解

3.1 注解作用说明

注解 描述
Before 前置增强,在目标方法调用前执行。
After 后置增强,在目标方法调用后执行,即使目标方法抛出异常也会执行,相当于finally。
Around 环绕增强,可以自行控制目标方法的调用时机,同时实现其他4种注解的效果,相当于MethodInterceptor。
AfterReturning 后置增强,在目标方法正常调用后执行,如果目标方法抛出异常则不会执行。
AfterThrowing 异常抛出增强,在目标方法抛出异常后执行,但不会抑制目标方法抛出的异常,异常会继续向上抛出。

注:实际使用时可按需使用,不是每次都用Around来cover所有连接点。

3.2 注解属性和用途

Advice注解拥有的属性如下:

注解 value argNames pointcut returning throwing
Before
After
Around
AfterReturning
AfterThrowing

3.2.1 value属性

用于配置pointcut,匹配哪些方法要被拦截。

例子:

java 复制代码
    @Before(value = "execution(public * com.kengcoder.springframeawsome.aop.service..*(..))")
    public void before(JoinPoint joinPoint) {

这段pointcut的含义是:指定包和其子包下的所有公共方法都被拦截。

3.2.2 pointcut属性

作用和value一样,如果没有配置则使用value,否则优先使用pointcut。

匹配代码在aspectjweaver依赖包中AjTypeImpl.java,如下:

java 复制代码
	private Advice asAdvice(Method method) {
		if (method.getAnnotations().length == 0) return null;
		Before beforeAnn = method.getAnnotation(Before.class);
		if (beforeAnn != null) return new AdviceImpl(method,beforeAnn.value(),AdviceKind.BEFORE);
		After afterAnn = method.getAnnotation(After.class);
		if (afterAnn != null) return new AdviceImpl(method,afterAnn.value(),AdviceKind.AFTER);
		AfterReturning afterReturningAnn = method.getAnnotation(AfterReturning.class);
		if (afterReturningAnn != null) {
			String pcExpr = afterReturningAnn.pointcut();
			if (pcExpr.equals("")) pcExpr = afterReturningAnn.value();
			return new AdviceImpl(method,pcExpr,AdviceKind.AFTER_RETURNING,afterReturningAnn.returning());
		}
		AfterThrowing afterThrowingAnn = method.getAnnotation(AfterThrowing.class);
		if (afterThrowingAnn != null) {
			String pcExpr = afterThrowingAnn.pointcut();
			if (pcExpr == null) pcExpr = afterThrowingAnn.value();
			return new AdviceImpl(method,pcExpr,AdviceKind.AFTER_THROWING,afterThrowingAnn.throwing());
		}
		Around aroundAnn = method.getAnnotation(Around.class);
		if (aroundAnn != null) return new AdviceImpl(method,aroundAnn.value(),AdviceKind.AROUND);
		return null;
	}

3.2.3 argNames属性

这个参数不是必须的,源码的注释如下:

java 复制代码
    /**
     * When compiling without debug info, or when interpreting pointcuts at runtime,
     * the names of any arguments used in the advice declaration are not available.
     * Under these circumstances only, it is necessary to provide the arg names in 
     * the annotation - these MUST duplicate the names used in the annotated method.
     * Format is a simple comma-separated list.
     * 
     * @return the argument names (should match the annotated method parameter names)
     */
    String argNames() default "";

通常情况下使用args就可以获取到目标方法的参数,下面举例说明。

目标方法:

java 复制代码
    @MyAnnotation
    public Book queryBook(String name, String author) {
        return authorBookDataMap.get(name, author);
    }

advice:

java 复制代码
    @Before(value = "@annotation(com.kengcoder.aop.annotation.MyAnnotation) && args(a1, a2)")
    public void befor5(String a1, String a2) {
        log.info("before: @annotation(com.kengcoder.springframeawsome.aop.annotation.MyAnnotation)");
        log.info("before: method param author: " + a1);
        log.info("before: method param name: " + a2);
    }

args的参数个数和目标方法参数个数一致,参数类型的顺序要保持一致,参数名不需要相同,advice方法的参数名需要和args参数名一致。

3.2.4 returning属性

用于获取被拦截的目标方法的返回值。

例子:

java 复制代码
    @AfterReturning(value = "com.kengcoder.aop.Pointcuts.tradingOperation()", returning = "result")
    public void afterReturning(Object result) {
        if (result != null) {
            log.info("afterReturning result: " + result.toString());
        }
    }

returning属性取值必须和advice方法的Object类型参数名完全一致。

3.2.5 throwing属性

用于获取被拦截的目标方法抛出的异常。

例子:

java 复制代码
    @AfterThrowing(pointcut = "com.kengcoder.aop.Pointcuts.tradingOperation()", throwing = "ex")
    public void afterThrowing(Exception ex) {
        log.info("exception: " + ex.getMessage());
    }

throwing属性取值必须和advice方法的Exception类型参数名完全一致。

3.3 joinpoint参数

Advice注解的方法可以将JoinPoint作为参数,说明如下:

注解 JoinPoint ProceedingJoinPoint
Before 可选参数
After 可选参数
Around 必选参数
AfterReturning 可选参数
AfterThrowing 可选参数

JoinPoint和ProceedingJoinPoint的使用在基础概念一章中已经有代码示例,不在赘述。

注:ProceedingJoinPoint继承自JoinPoint,因此也可以获取相应的JoinPoint信息。

4. Pointcut

4.1 常用表达式

4.1.1 execution-匹配方法

作用是用于匹配要拦截的目标方法,通过设置通配符也能实现匹配类的效果。

● 表达式

表达式 说明
execution(* com.kengcoder.EmployeeManager.*(..)) 指定包下的指定类的所有方法
execution(* com.kengcoder.IBookService+.*(..)) 指定包下指定接口及其实现的所有方法
execution(* com.kengcoder...(..)) 匹配指定包和其所有子包的所有类的所有方法
execution(public * EmployeeManager.*(..)) 指定类的公共方法
execution(public Employee EmployeeManager.*(..)) 指定类和指定返回类型的公共方法
execution(public Employee EmployeeManager.*(Employee, ..)) 指定类和指定返回类型,且指定了第1个参数类型的公共方法
execution(public Employee EmployeeManager.*(Employee, Integer)) 指定类和指定返回类型,且指定了所有参数类型的公共方法

● 匹配规则

规则 说明
前面的* 表示方法修饰符和返回类型,可以指定其中一个,也可以两个都指定。
后面的* 匹配所有方法。
中间的..* 匹配包及其子包下的所有类。
+ 接口及其实现类。
后面的(..) 用于匹配方法参数类型,..表示所有参数不需要匹配参数类型。

● 参数匹配例子

规则 说明
(..) 有任意参数,不匹配参数类型。
(Employee) 匹配1个参数,参数类型是Employee。
(Employee, String) 匹配2个参数,第1个参数类型是Employee,第2个参数类型是String。
(Employee, ..) 匹配多个参数,第1个参数类型是Employee,后面的多个参数不需要匹配参数类型。
(.., Employee) 匹配多个参数,前面的多个参数不需要匹配参数类型,最后1个参数类型是Employee。
() 匹配没有任何参数的方法。

4.1.2 within-匹配类

作用是匹配要拦截的目标类。

● 表达式

表达式 说明
within(com.kengcoder.*) 指定包下所有类的所有方法。
within(com.kengcoder..*) 指定包以及其子包下的所有类的所有方法。
within(com.kengcoder.EmployeeManager) 指定包下指定类的所有方法。
within(com.kengcoder.IEmployeeManager+) 指定接口的所有实现类的所有方法

● 匹配规则

规则 说明
.* 包下的所有类
..* 包和其子包下的所有类
+ 接口和所有实现类

4.1.3 匹配Bean

作用是匹配要拦截的Bean。

● 表达式

表达式 说明
bean(employeeManager) 指定名称的bean

4.1.4 @within-匹配方法声明类上的注解

作用是匹配有指定注解的要被拦截的类。

● 表达式

表达式 说明
@within(com.kengcoder.MyAnnotation) 有指定注解的类,必须是完全路径。

4.1.5 @annotation-匹配方法上的注解

作用是匹配有指定注解的要被拦截的方法。

● 表达式

表达式 说明
@annotation(com.kengcoder.MyAnnotation) 有指定注解的方法,必须是完全路径。

4.2 使用方式

pointcut有两种使用方式,如下:

● 方式1 Advice注解直接使用pointcut表达式,前面的例子均是此方式。

● 方式2 Advice注解引用由@Pointcut注解的方法,@Pointcut可以使用pointcut表达式,也可以引用它注解的其他方法,下面看个例子:

首先定义一个类专门用来配置pointcut:

java 复制代码
public class Pointcuts {
    // 直接使用pointcut表达式
    @Pointcut("execution(public * *(..))")
    public void publicMethod() {
    }

    // 直接使用pointcut表达式
    @Pointcut("within(com.kengcoder.service..*)")
    public void inTrading() {}

    // 可以引用@Pointcut定义的方法,并通过&&(and)、||(or)、!(非)组合在一起
    @Pointcut("publicMethod() && inTrading()")
    public void tradingOperation() {
        log.info("tradingOperation");
    }
}

然后在advice注解中引用@Pointcut注解的方法:

java 复制代码
    @Before(pointcut = "com.kengcoder.aop.Pointcuts.tradingOperation()")
    public void before() {
        // do something
    }

方式2相较方式1的优点是可以集中维护pointcut并复用,使得同一个pointcut规则可以被多个advice注解引用。

5. Aspect类执行顺序

当定义了多个Aspect类时,例如LoggingAspect、CacheAspect、SecurityAspect,Logging切面和cache切面要在Security切面之前被调用,此时就要控制切面的执行顺序。

控制顺序有两种方式:

● 方式1

使用Order注解指定顺序,数字越小优先级越高,例子:

java 复制代码
@Component  // 必须添加此注解
@Order(1)
@Aspect  //此注解用于标识是切面类,这个切面类可以包含多个advice方法(拦截目标对象方法后要做增强处理的方法)
public class LoggingAspect {

● 方式2

实现org.springframework.core.Ordered接口的getOrder方法,例子:

java 复制代码
@Component
@Aspect
public class SecurityAspect implements Ordered {

    @Before(value = "com.kengcoder.aop.Pointcuts.tradingOperation()")
    public void before() {
        log.info("before");
    }

    @Override
    public int getOrder() {
        return 3;
    }
}

6. 小结

本文介绍了AOP的基本概念,如何启用注解拦截能力,Advice注解的作用和使用,Pointcut的两种使用方式,以及Aspect类执行顺序的控制逻辑。

这些内容基本已经覆盖了spring通过注解实现拦截的方方面面,大部分拦截场景都可以支持,不过就拦截能力而言,还有其他的方式,例如在spring中通过编码实现拦截,filter,HandlerInterceptor,java agent等等,当某个场景通过spring注解方式实现不了时,还可以看看其他方式是否可以实现。


其他阅读:

如何编写软件设计文档
Spring Cache架构、机制及使用
布隆过滤器适配Spring Cache及问题与解决策略
JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(五)函数式接口-复用,解耦之利刃

相关推荐
公贵买其鹿22 分钟前
List深拷贝后,数据还是被串改
java
向前看-3 小时前
验证码机制
前端·后端
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹4 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭5 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫5 小时前
泛型(2)
java
超爱吃士力架5 小时前
邀请逻辑
java·linux·后端
南宫生5 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石5 小时前
12/21java基础
java
李小白665 小时前
Spring MVC(上)
java·spring·mvc