1 简述
切面编程AOP,是通过一种非侵入式方式,对目标类的方法进行影响或逻辑增强。通过AOP框架,可以更简洁的解决该问题。
切面编程,常给人一种感觉,它可以不知不觉对当前类的方法,做后期增强、补充处理。
切面编程真正的价值,在于它也是一种抽象设计方法,就像面向对象编程的核心是对类的抽象、设计。切面编程也一样,核心是对切面的抽象、设计。
spring aop只是解决"切面"问题的一种方法、工具。就像C语言,也可以进行面向对象的设计、开发,后续文章会介绍其它基于"切面思想"的手段、方法。
下面是两种设计方法的定义:
OOP是对业务流程中对象(属性、方法)的抽象封装,基本单位是类。
AOP是对业务流程中横向逻辑的提取封装,基本单位是切面。
在抽象设计中,最核心的是类和切面,但二者都必须基于业务、流程的设计、定义。
spring aop构建在动态代理基础之上,通过JDK动态代理和CGLIB两种方式来实现代理类。
代理类可以对目标类进行业务逻辑增强,也就是织入切面逻辑。
注:spring aop只针对类的方法进行逻辑增强。
2 术语
像大多数技术一样,切面编程也有自己特有的术语,如:连接点、切点、通知、切面、织入等,有些不太直观。
这些术语主要是想表达,在何地、何时,织入增量逻辑。一般情况下,只需要理解切点、通知、切面这3个核心术语就足够了。
切点(pointcut)
它解决在何地,也就是在那些类的方法,织入切面逻辑,它的主要职责就是,通过类、方法、参数3个维度,以及这3个维度上的注解,来具体的匹配、定位类的方法。
通知(advice)
它解决在何时,也就是切点所匹配方法执行的前、后、异常等时候,执行切面逻辑。
切面(aspect)
可以这么理解,通知和切点,以及增量逻辑,共同定义了切面的全部内容,也就是,它是什么,在何时、何处完成其切面逻辑。
简化公式:切面 = 切点 + 通知 + 增量逻辑。
3 规则
Spring可以通过 xml配置 或 @Aspect注解 两种风格,来定义切面。
@Aspect注解风格,是将切面作为带有切面注解的普通Java类来声明的一种风格。
以下是具体规则。
3.1 切点
它是解决在什么地方,也就是如何来定位类的方法的范围问题,这就需要用到切点表达式,以下是规则:
execution: 用于匹配方法执行的连接点。这是在使用Spring AOP时要使用的主要切点指定器。
within: 将匹配限制在某些类型内的连接点(使用Spring AOP时,执行在匹配类型内声明的方法)。
this: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中bean引用(Spring AOP代理)是给定类型的实例。
target: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中目标对象(被代理的应用程序对象)是给定类型的实例。
args: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中参数是给定类型的实例。
@target: 限制匹配到连接点(使用Spring AOP时方法的执行),其中执行对象的类有一个给定类型的注解。
@args: 将匹配限制在连接点(使用Spring AOP时方法的执行),其中实际传递的参数的运行时类型有给定类型的注解。
@within: 将匹配限制在具有给定注解的类型中的连接点(使用Spring AOP时,执行在具有给定注解的类型中声明的方法)。
@annotation: 将匹配限制在连接点的主体(Spring AOP中正在运行的方法)具有给定注解的连接点上。
上面是官方文档的定义,看着有些晕。
从本质理解,就是如何通过类、方法、参数等维度,让切点去匹配具体的method,归纳如下:
a. 通过execution表达式来匹配方法,@annotation表达式来匹配方法的注解。(最常用)
b. 通过within表达式来匹配类,@within表达式来匹配类的注解。
c. 通过args表达式来匹配方法参数的实例,@args表达式来匹配方法参数类型上的注解。(区别于在方法上定义的参数注解)
3.2 通知
它是解决在什么时候,也就是来定义,在切点所匹配的方法,执行的前、后、异常等时候,来执行切面逻辑,以下是规则:
@Before: 通知方法会在目标方法之前执行。
@After:通知方法会在目标方法正常返回或抛出异常后执行。
@AfterReturning: 通知方法会在目标方法正常返回后执行。
@AfterThrowing: 通知方法会在目标方法抛异常后执行。
@Around: 通知方法会将目标方法封装起来。
3 实践
3.1 配置
开启
启用 @Aspect注解 支持,需要添加 @EnableAspectJAutoProxy注解 配置,如下:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig { }
提示:spring boot项目已默认打开,不添加,@Aspect也会起作用。
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 例子-切点
匹配类、接口、方法:
/*
* 匹配方法: 匹配mtr.demo包、及子包下,所有bean的hello方法 。
*/
@Pointcut("execution(public * mtr.demo..*.hello(..))")
public void pointCut1() {
}
/*
* 匹配方法: 仅匹配 mtr.demo包下,所有bean的hello方法 。
*/
@Pointcut("execution(public * mtr.demo.*.hello(..))")
public void pointCut2() {
}
/*
* 匹配方法: 匹配mtr.demo包、及子包下,所有bean的hello方法, 且第一个参数必须是String类型。
*/
@Pointcut("execution(public * mtr.demo..*.hello(String, ..))")
public void pointCut3() {
}
/*
* 匹配类: 匹配包 mtr.demo、及子包下,所有bean的方法
*/
@Pointcut("within(mtr.demo..*)")
public void pointCut4() {
}
/*
* 匹配接口: 匹配包 mtr.demo、及子包下,所有实现UserService接口bean的方法
*/
@Pointcut("within(mtr.demo..*) && this(mtr.demo.service.UserService)")
public void pointCut5() {
}
/*
* 组合形式
*/
@Pointcut("pointCut1() && pointCut5()")
public void pointCut6() {
}
匹配注解:
/*
* 匹配方法的注解: 所有bean中被RequestMapping注解的方法
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void pointCut6() {
}
/*
* 匹配类的注解: 所有被RequestMapping注解的bean中的方法
*/
@Pointcut("@within(org.springframework.web.bind.annotation.RequestMapping)")
public void pointCut7() {
}
3.3 例子-通知
不传递参数:
@Component
@Aspect
public class DemoAspect1 {
/*
* 匹配方法: 匹配mtr.demo包、及子包下,所有bean的hello方法 。
*/
@Pointcut("execution(public * mtr.demo..*.hello(..))")
public void pointCut1(){}
@Before("pointCut1()")
public void doBefore(JoinPoint joinPoint) {
System.out.println("Aspect-doBefore,this.class:" + joinPoint.getThis().getClass());
}
@After("pointCut1()")
public void doAfter(JoinPoint joinPoint) {
System.out.println("Aspect-doAfter,this.class:" + joinPoint.getThis().getClass());
}
@AfterReturning(pointcut="pointCut1()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("Aspect-doAfterReturning,result:" + result);
}
@AfterThrowing(pointcut="pointCut1()", throwing="t")
public void doAfterThrowing(JoinPoint joinPoint, Throwable t) {
System.out.println("Aspect-doAfterThrowing,t:" + t.getMessage());
}
@Around("pointCut1()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
Object proceed = null;
try {
System.out.println("Aspect-doAround-begin,this.class:" + joinPoint.getThis().getClass());
proceed = joinPoint.proceed();
System.out.println("Aspect-doAround-end,this.class:" + joinPoint.getThis().getClass());
} catch (Throwable e) {
System.out.println("Aspect-doAround-exception,this.class:" + joinPoint.getThis().getClass());
throw e;
}
return proceed;
}
}
传递参数:
@Component
@Aspect
public class DemoAspect2 {
/*
* 匹配方法: 匹配mtr.demo.service包、及子包下,所有bean的方法, 且第一个参数必须是String类型。
*/
@Pointcut("execution(public * mtr.demo.service..*.*(..)) && args(x,..)")
public void pointCut1(String x){}
@Before("pointCut1(x)")
public void doBefore(JoinPoint joinPoint, String x) {
System.out.println("Aspect-doBefore,parameter x:" + x);
}
@After("pointCut1(x)")
public void doAfter(JoinPoint joinPoint, String x) {
System.out.println("Aspect-doAfter,parameter x:" + x);
}
}
4 案例
4.1 注入userId
web项目,常需要从会话中解析出userId,然后提供给后续的业务逻辑,以下通过AOP的方式,来处理该问题。
思路:新增一个注解@UserId,然后通过该注解,标记需要接收UserID参数的方法,最后通过AOP的方式,来注入userId值,以下是处理逻辑。
@UserId注解类
@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId {
String value() default "";
}
切面UserAspect类,负责拦截web接口方法,注入userId值。
@Component
@Aspect
public class UserAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)"
+ "|| @annotation(org.springframework.web.bind.annotation.GetMapping)"
+ "|| @annotation(org.springframework.web.bind.annotation.PostMapping)"
+ "|| @annotation(org.springframework.web.bind.annotation.PutMapping)"
+ "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
)
public void pointCutForUserId() { }
@Around("pointCutForUserId()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
Object result = null;
try {
// 请求参数
Object[] args = joinPoint.getArgs();
// 注入userId
int indexOfUserId = this.getIndexOfUserIdParameter(joinPoint);
if (indexOfUserId > -1) {
args[indexOfUserId] = this.parseUserIdFromSession();
}
result = joinPoint.proceed(args);
} catch (Throwable e) {
throw e;
}
return result;
}
private int getIndexOfUserIdParameter(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Annotation[][] parameterAnnotations = methodSignature.getMethod().getParameterAnnotations();
for(int i = 0; i < parameterAnnotations.length; i++) {
for(int j = 0; j < parameterAnnotations[i].length; j++) {
Annotation a = parameterAnnotations[i][j];
if (a.annotationType() == UserId.class) {
return i;
}
}
}
return -1;
}
private Integer parseUserIdFromSession() {
return 123;
}
}
web接口类UserController
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping(value="/get")
@ResponseBody
public User get(@UserId Integer userId) {
System.out.println("paramter userId: " + userId);
return new User(userId);
}
}