铿然架构 | 作者 / 铿然 这是 铿然架构 的第 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基础(五)函数式接口-复用,解耦之利刃