Spring Boot AOP - 面向切面编程

5,898 阅读7分钟

AOP,即面向切面编程,其核心思想就是把业务分为核心业务非核心业务两大部分。例如一个论坛系统,用户登录、发帖等等这是核心功能,而日志统计等等这些就是非核心功能。

在Spring Boot AOP中,非核心业务功能被定义为切面,核心和非核心功能都开发完成之后,再将两者编织在一起,这就是AOP。

AOP的目的就是将那些与业务无关,却需要被业务所用的逻辑单独封装,以减少重复代码,减低模块之间耦合度,利于未来系统拓展和维护。

今天,我将做一个简单的打印用户信息的程序,即后端接受POST请求中的User对象将其打印这样一个逻辑,在这个上面实现AOP。

首先放上用户打印服务逻辑的方法代码:

Service层:

package com.example.springbootaop.service.impl;

import com.example.springbootaop.model.User;
import com.example.springbootaop.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class UserServiceImpl implements UserService {

   /**
    * 使用Logger
    */
   private Logger logger = LoggerFactory.getLogger(this.getClass());

   @Override
   public void printUserInfo(User user) {
      logger.info("用户id:" + user.getId());
      logger.info("用户名:" + user.getUsername());
      logger.info("用户昵称:" + user.getNickname());
   }

}

Control层:

package com.example.springbootaop.api;

import com.example.springbootaop.model.User;
import com.example.springbootaop.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserAPI {

   @Autowired
   private UserService userService;

   @PostMapping("/user")
   public String printUser(@RequestBody User user) {
      userService.printUserInfo(user);
      return "已完成打印!";
   }

}

User是一个简单的封装类,这里就不展示了,文章末尾会给出整个示例程序代码地址。

1,AOP到底有什么不同

这里只是实现一个简单的逻辑,打印用户信息,这就是我们今天的核心功能。

如果这个时候我们要给它加上非核心功能:在打印之前和打印之后分别执行一个方法,如果你不知道AOP,你可能会把Control层的方法改成如下形式:

@PostMapping("/user")
public String printUser(@RequestBody User user) {
   // 执行核心业务之前
   doBefore();
   // 执行核心业务
   userService.printUserInfo(user);
   // 执行核心业务之后
   doAfter();
   ...
   return "已完成打印!";
}

如果说方法多了,业务多了,非核心业务的逻辑一变,所有Controller的全部方法都要改动,非常麻烦,且代码冗余,耦合度高。

这时,就需要AOP来解决这个问题。

AOP只需要我们单独定义一个切面,在里面写好非核心业务的逻辑,即可将其织入核心功能中去,无需我们再改动Service层或者Control层。

2,AOP中的编程术语和常用注解

在学习AOP之前,我们还是需要了解一下常用术语:

  • 切面:非核心业务功能就被定义为切面。比如一个系统的日志功能,它贯穿整个核心业务的逻辑,因此叫做切面
  • 切入点:在哪些类的哪些方法上面切入
  • 通知:在方法执行前/后或者执行前后做什么
    • 前置通知:在被代理方法之前执行
    • 后置通知:在被代理方法之后执行
    • 返回通知:被代理方法正常返回之后执行
    • 异常通知:被代理方法抛出异常时执行
    • 环绕通知:是AOP中强大、灵活的通知,集成前置和后置通知
  • 切面:在什么时机、什么地方做什么(切入点+通知)
  • 织入:把切面加入对象,是生成代理对象并将切面放入到流程中的过程(简而言之,就是把切面逻辑加入到核心业务逻辑的过程)

在Spring Boot中,我们使用@AspectJ注解开发AOP,首先需要在pom.xml中引入如下依赖:

<!-- Spring Boot AOP -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后就可以进行AOP开发了!

这里先给出常用注解,大家联系着下面的例子看就可以了:

  • @Pointcut 定义切点
  • @Before 前置通知
  • @After 后置通知
  • @AfterReturning 返回通知
  • @AfterThrowing 异常通知
  • @Around 环绕通知

3,定义切面

新建aop包,在里面新建类作为我们的切面类,先放出切面类代码:

package com.example.springbootaop.aop;

import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspect {

   /**
    * 日志打印
    */
   private Logger logger = LoggerFactory.getLogger(this.getClass());

   /**
    * 使用Pointcut给这个方法定义切点,即UserService中全部方法均为切点。<br>
    * 这里在这个log方法上面定义切点,然后就只需在下面的Before、After等等注解中填写这个切点方法"log()"即可设置好各个通知的切入位置。
    * 其中:
    * <ul>
    *     <li>execution:代表方法被执行时触发</li>
    *     <li>*:代表任意返回值的方法</li>
    *     <li>com.example.springbootaop.service.impl.UserServiceImpl:这个类的全限定名</li>
    *     <li>(..):表示任意的参数</li>
    * </ul>
    */
   @Pointcut("execution(* com.example.springbootaop.service.impl.UserServiceImpl.*(..))")
   public void log() {

   }

   /**
    * 前置通知:在被代理方法之前调用
    */
   @Before("log()")
   public void doBefore() {
      logger.warn("调用方法之前:");
      logger.warn("接收到请求!");
   }

   /**
    * 后置通知:在被代理方法之后调用
    */
   @After("log()")
   public void doAfter() {
      logger.warn("调用方法之后:");
      logger.warn("打印请求内容完成!");
   }

   /**
    * 返回通知:被代理方法正常返回之后调用
    */
   @AfterReturning("log()")
   public void doReturning() {
      logger.warn("方法正常返回之后:");
      logger.warn("完成返回内容!");
   }

   /**
    * 异常通知:被代理方法抛出异常时调用
    */
   @AfterThrowing("log()")
   public void doThrowing() {
      logger.error("方法抛出异常!");
   }

}

切面类需要打上@Aspect注解表示这是一个切面类,然后不要忘了打上@Component注解。

我们逐步来看。

首先是定义切点,只需定义一个空方法,在上面使用@Pointcut注解即可,注解里面内容含义如下:

  • execution 代表方法被执行时触发
  • * 代表任意返回值的方法
  • com.example.springbootaop.service.impl.UserServiceImpl 被织入类的全限定名
  • (..) 表示任意的参数

定义完切点之后,就可以定义各个通知的方法逻辑了,这些就是我们的切面逻辑,也就是非核心业务的逻辑。

上面在doBefore方法上面,我们使用了@Before注解,这样就标明了doBefore方法是前置通知逻辑,会在被织入方法之前执行。我们把log方法定义为切入点,然后下面各个通知注解中,填写这个切入点方法名称即可。

我们也并不需要定义所有的通知,只需定义需要的即可。

其实,如果不定义上面的切入点方法log@Pointcut,你仍然可以把execution表达式直接写在各个通知的注解里面,例如:

/**
 * 前置通知:在被代理方法之前调用
 */
@Before("execution(* com.example.springbootaop.service.impl.UserServiceImpl.*(..))")
public void doBefore(JoinPoint joinPoint) {
   logger.warn("调用方法之前:"); logger.warn("接收到请求!");
}

但是大多数情况并不推荐这样,这种写法较为复杂。

我们发送一个请求测试一下:

image.png

通过这个,我们也可以发现各个通知的执行顺序:

Before -> AfterReturning -> After

4,环绕通知

环绕通知是AOP中最强大的通知,可以同时实现前置和后置通知,不过它的可控性没那么强,如果不用大量改变业务逻辑,一般不需要用到它。我们在上述切面加入下列环绕通知方法:

/**
 * 环绕通知
 */
@Around("log()")
public void around(ProceedingJoinPoint joinPoint) {
   logger.warn("执行环绕通知之前:");
   try {
      joinPoint.proceed();
   } catch (Throwable e) {
      e.printStackTrace();
   }
   logger.warn("执行环绕通知之后");
}

通知方法中有一个ProceedingJoinPoint类型参数,通过其proceed方法来调用原方法。需要注意的是环绕通知是会覆盖原方法逻辑的,如果上面代码不执行joinPoint.proceed();这一句,就不会执行原被织入方法。因此环绕通知一定要调用参数的proceed方法,这是通过反射实现对被织入方法调用。

再次测试如下:

image.png

5,通知方法传参

上面每个通知方法是没有参数的。其实,通知方法是可以接受被织入方法的参数的。我们上述被织入方法参数就是一个User对象,因此通知方法也可以加上这个参数接受。我们改变前置通知方法如下:

/**
 * 前置通知:在被代理方法之前调用
 */
@Before("log() && args(user)")
public void doBefore(User user) {
   logger.warn("调用方法之前:");
   logger.warn("接收到请求!");
   logger.warn("得到用户id:" + user.getId());
}

测试结果:

image.png

可见在注解后面加一个args选项,里面写参数名即可。

需要注意的是,通知方法的参数必须和被织入方法参数一一对应例如:

/**
 * 被织入方法
 * /
public void print(User user, int num) {
   ...
}

/**
 * 通知
 * /
@Before("log() && args(user, num)")
public void doBefore(User user, int num) {
   ...
}

6,总结

AOP其实使用起来是个很方便的东西,大大降低了相关功能之间的耦合度,使得整个系统井井有条。

定义切面,然后定义切点,再实现切面逻辑(各个通知方法),就完成了一个简单的切面。

示例程序仓库地址