Spring Framework 学习笔记4:AOP
1.概念
AOP(Aspect Oriented Programming,面向切面编程)是一种编程思想。它要解决的问题是:如何在不改变代码的情况下增强代码的功能。
AOP 有一些核心概念:
- 连接点(JoinPoint):理论上可以是代码运行的任意位置,比如变量声明。但在 Spring AOP 的实现中,只能是方法。
- 切入点(Pointcut):要增强功能的地方,对应一个或多个连接点。
- 通知(Advice):所增强的功能会在通知中定义。
- 切面(Aspect):在切面中关联接入点和所执行的通知。
更详细的说明可以观看这个视频。
2.快速入门
下面通过一个简单示例项目说明如何在 Spring 框架中实现 AOP。
2.1.准备工作
先下载示例项目 aop-demo 并解压。
这是一个用 Maven 搭建的 Spring 项目,有一些基本的实体类、Service 以及测试用例。
UserServiceTests
内容如下:
@Service
public class UserServiceImpl implements UserService {
@Override
public void add(User user) {
user.setId(1);
System.out.println("%s was added.".formatted(user));
}
@Override
public void deleteById(int id) {
System.out.println("User(%d) was deleted.");
}
}
可以执行测试套件UserServiceTests
对UserService
的两个方法进行测试。这两个方法没有实际功能,只是输出一些模拟信息:
User(id=1, name=icexmoon, age=18) was added.
User(%d) was deleted.
现在我们用 Spring AOP 为这两个方法添加上额外功能:在方法执行前输出当前时间。
2.2.依赖
Spring AOP 使用的是 spring-aop 这个依赖,不过我们并不需要添加,因为该依赖已经包含在 Spring 框架( spring-context 这个依赖)中:
但我们还需要添加一个 AspectJ 的依赖,因为 Spring AOP 使用了 AspectJ 定义的一系列注解:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
</dependency>
注意,从 MavenRepository 检索出来的依赖指定了
scope
是runtime
,要去掉。否则无法在编码阶段使用 AspectJ 的一系列注解。
2.3.切入点
定义一个切入点:
public class TimeAspect {
@Pointcut("execution(public void cn.icexmoon.aopdemo.service.UserService.add(..))")
private void userAdd(){}
}
切入点本身是一个空方法,只不过在这个方法上用一个@Pointcut
注解定义了切入点关联的连接点信息。
在上边这个示例中,切入点关联的是UserService
接口的名称为add
的方法,且不限定方法参数列表。
2.4.通知
要想让这个切入点执行一些额外功能,需要定义一个通知:
public class TimeAspect {
// ...
@Before("userAdd()")
public void printTime(){
String timeString = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(timeString);
}
}
通知有多种类型,对应在连接点的不同阶段执行相应的行为,比如在连接点之前执行就需要使用@Before
注解定义的通知。其关联的切入点用value
属性定义。
2.5.切面
要让 Spring 运行我们定义好的通知,还需要为通知和切入点所在的切面类添加注解:
@Component
@Aspect
public class TimeAspect {
// ...
}
@Component
注解将这个类定义为 Bean,@Aspect
注解说明这个类是一个切面,其中定义了切入点和通知。
2.6.开启 AOP 功能
最后,还需要在核心配置类上添加@EnableAspectJAutoProxy
注解以开启 Spring AOP 功能:
@EnableAspectJAutoProxy
@Configuration
@ComponentScan(basePackages = "cn.icexmoon.aopdemo")
public class SpringConfig {
}
2.7.测试
现在运行测试用例,就可以看到在UserService.add
方法执行前,会输出当前时间:
2023-08-24T16:14:39.0459787
User(id=1, name=icexmoon, age=18) was added.
User(%d) was deleted.
也就是说,我们在没有改变原始代码的情况下增强了代码的功能。
这就是 AOP。
3.工作原理
AOP 是用代理实现的,具体流程为:
- Spring 容器启动
- 读取所有切面配置中的切入点
- 初始化 Bean,并判断 Bean 的方法是否与切入点匹配,如果匹配,为其创建代理对象。
- 执行 Bean 方法,如果是原始对象,直接执行。如果是代理对象,执行代理对象(被增强过的)方法。
详细说明可以观看这个视频。
4.切入点表达式
切入点上用切入点表达式描述切入点关联的连接点(方法)。
这里只展示一个简单示例,可以将之前的示例改写为:
@Component
@Aspect
public class TimeAspect {
/**
* 切入点,匹配任意 service 层方法调用
*/
@Pointcut("execution(* cn.icexmoon.aopdemo.service.*Service.*(..))")
private void anyServiceMethods(){}
@Before("anyServiceMethods()")
public void printTime(){
String timeString = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(timeString);
}
}
现在任意的 Service 层方法(public)执行前都会打印时间。
5.通知类型
Spring AOP 的通知类型有:
- @Before
- @After
- @Around
- @AfterReturn
- @AfterThrow
6.案例
6.1.统计方法执行时长
@Component
@Aspect
public class TimeAspect {
// ...
@Around("anyServiceMethods()")
public Object clockExecuteTime(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
long begin = System.currentTimeMillis();
Object result = pjp.proceed();
long end = System.currentTimeMillis();
System.out.printf("Method %s.%s() is executed, use %d mills.%n",
className,
methodName,
end - begin);
return result;
}
}
6.2.处理方法参数
有时候,一些内容来自用户录入,用户可能会在有意或无意间在有效信息前后添加一些空白符,通常我们需要手动调用String.trim()
方法对参数进行处理。
可以利用 AOP 简化这种处理:
@Component
@Aspect
public class StrAspect {
/**
* 任意方法
*/
@Pointcut("execution(* *..*(..))")
private void anyMethod() {
}
/**
* 对任意使用了 @TrimParams 注解的方法,检查其参数,如果是 String,进行 trim 处理
*
* @param pjp
* @param annotation
* @return
* @throws Throwable
*/
@Around(value = "anyMethod() && @annotation(annotation)")
public Object trimParams(ProceedingJoinPoint pjp, TrimParams annotation) throws Throwable {
Object[] args = pjp.getArgs();
for (int i = 0; i < args.length; i++) {
Object currentArg = args[i];
// 如果参数类型是字符串,进行 trim 处理
if (currentArg instanceof String) {
String strArg = (String) currentArg;
args[i] = strArg.trim();
}
}
Object result = pjp.proceed(args);
return result;
}
}
这里定义了一个通知,用于处理方法中的字符串类型的参数,并去除其前后的空白符。
为了便于控制,这里引入了一个自定义注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TrimParams {
}
现在只要添加了该注解的方法,就会被上面定义的通知处理:
@Service
public class UserServiceImpl implements UserService {
// ...
@Override
@TrimParams
public void printMsg(String msg) {
System.out.printf("msg:[%s]%n", msg);
}
}
可以用下面的测试用例观察是否生效:
// ...
public class UserServiceTests {
// ...
@Test
public void testPrintMsg(){
userService.printMsg(" 123 ");
}
}
The End,谢谢阅读。
本文的完整示例可以从这里获取。