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基础(五)函数式接口-复用,解耦之利刃

相关推荐
葫芦和十三4 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp4 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑5 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯6 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan8 小时前
多Agent之间的区别
后端
青石路9 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充10 小时前
1.面向对象设计思想
后端
IT_陈寒10 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro11 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗11 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端