Spring Framework 学习笔记4:AOP

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.");
    }
}

可以执行测试套件UserServiceTestsUserService的两个方法进行测试。这两个方法没有实际功能,只是输出一些模拟信息:

User(id=1, name=icexmoon, age=18) was added.
User(%d) was deleted.

现在我们用 Spring AOP 为这两个方法添加上额外功能:在方法执行前输出当前时间。

2.2.依赖

Spring AOP 使用的是 spring-aop 这个依赖,不过我们并不需要添加,因为该依赖已经包含在 Spring 框架( spring-context 这个依赖)中:

Spring Framework 学习笔记4:AOP_第1张图片

但我们还需要添加一个 AspectJ 的依赖,因为 Spring AOP 使用了 AspectJ 定义的一系列注解:

<dependency>
    <groupId>org.aspectjgroupId>
    <artifactId>aspectjweaverartifactId>
    <version>1.9.19version>
dependency>

注意,从 MavenRepository 检索出来的依赖指定了scoperuntime,要去掉。否则无法在编码阶段使用 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 是用代理实现的,具体流程为:

  1. Spring 容器启动
  2. 读取所有切面配置中的切入点
  3. 初始化 Bean,并判断 Bean 的方法是否与切入点匹配,如果匹配,为其创建代理对象。
  4. 执行 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框架教程

你可能感兴趣的:(JAVA,spring,aop)