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,谢谢阅读。
本文的完整示例可以从这里获取。
7.参考资料
- 从零开始 Spring Boot 32:AOP II - 红茶的个人站点 (icexmoon.cn)
- 黑马程序员SSM框架教程



















