在引入Spring AOP之前,我们先来解释什么是切面。所谓切面,本质上解决的是关注点分离的问题。而面向切面编程可以说是面向对象编程的一种补充,目标是将一个应用程序抽象成各个切面。针对图3-1中所示的应用场景,可以引入AOP的思想把事务处理和安全控制等非功能性需求从业务逻辑中拆分出来,构成独立的关注点,如图3-2所示。
图3-2 面向切面思想示意图
从图3-2中可以很形象地看出,所谓切面相当于对象间的横切点,我们可以将其抽象为单独的模块进行开发和维护。
AOP只是一种设计理念,虽然概念不复杂,但实现过程不那么简单。而Spring作为AOP的一款具体实现框架,也提供了自身的一些设计思想和编程组件。本小节将对Spring AOP中的核心概念展开讨论,为下一小节的使用示例提供基础。
为了理解AOP的具体实现过程,我们首先需要引入一组特定的术语,包括连接点(JoinPoint)、通知(Advice)、切点(PointCut)以及切面(Aspect)。
在AOP中,连接点表示应用执行过程中能够插入切面的一个点。这种连接点可以是方法调用、异常处理、类初始化或对象实例化。
所谓通知,描述的是切面何时执行以及如何执行对应的业务逻辑。通知有很多种类型,比方说在方法执行之前、之后、前后及执行完成时进行通知,或者在方法执行异常时进行通知。
请注意,通知不一定应用于所有连接点,所以我们引入了切点的概念。切点是连接点的集合,用于定义必须执行的通知。因此切点为组件在应用程序中执行具体的通知提供了细粒度控制的方法。
最后,通知和切点组合在一起就构成了切面。切面用于定义应用程序中的业务逻辑及其执行的位置。
以上概念比较抽象,我们通过图来使这些概念具象化,如图3-3所示。
图3-3 AOP核心概念示意图
Spring框架对图3-3中的概念都进行了实现,但也有自身的一些限制。例如,连接点只支持方法的调用。针对通知,Spring专门提供了一组注解,包括@Before、@After、@Around、@AfterReturning和@AfterThrowing等,分别对应方法执行的各个阶段。切点的定义是和业务流程执行紧密相关的,所以在Spring中,可以通过使用各种灵活的表达式来定义切点。最后,Spring专门提供了一个@Aspect注解来定义切面。
在理解了AOP的相关概念以及Spring框架所提供的各种注解之后,在本节中,我们将通过代码示例来展示注解的使用方法。
现在,假设有一个代表用户账户的AccountService接口,如代码清单3-1所示。
代码清单3-1 AccountService接口代码
public interface AccountService { boolean doAccountTransaction(Account source, Account dest, int amount) throws MinimumAmountException; }
可以看到,在该AccountService接口中定义了一个用于实现账户交易的doAccount-Transaction()方法。然后我们提供它的实现类,如代码清单3-2所示。
代码清单3-2 AccountService接口实现类代码
public class AccountServiceImpl implements AccountService { private static final Logger LOGGER = Logger.getLogger(AccountServiceImpl.class); @Override public boolean doAccountTransaction(Account source, Account dest, int amount) throws MinimumAmountException { LOGGER.info("执行交易"); if (amount < 10) { throw new MinimumAmountException("交易金额过少"); } return true; } }
在doAccountTransaction()方法中,我们在执行交易之前记录了操作日志,这种实现方式看上去没有什么问题。如果针对交易操作,我们希望在该操作之前、之后、执行过程中以及抛出MinimumAmountException异常时都记录对应的日志,那么实现起来就没那么容易了。这个时候,可以通过AOP进行切入,并添加对应的日志记录。基于Spring AOP,其实现过程如代码清单3-3所示。
代码清单3-3 AccountServiceAspect代码
@Aspect public class AccountServiceAspect { private static final Logger LOGGER = Logger.getLogger(AccountServiceAspect.class); @Pointcut("execution(* com.springboot.aop.service.AccountService.doAccountTransaction(..))") public void doAccountTransaction() {} @Before("doAccountTransaction()") public void beforeTransaction(JoinPoint joinPoint) { LOGGER.info("交易前"); } @After("doAccountTransaction()") public void afterTransaction(JoinPoint joinPoint) { LOGGER.info("交易后"); } @AfterReturning(pointcut = "doAccountTransaction() and args(source, dest, amount)", returning = "isTransactionSuccessful") public void afterTransactionReturns(JoinPoint joinPoint, Account source, Account dest, Double amount, boolean isTransactionSuccessful) { if (isTransactionSuccessful) { LOGGER.info("转账成功 "); } } @AfterThrowing(pointcut = "doAccountTransaction()", throwing = "minimumAmountException") public void exceptionFromTransaction(JoinPoint joinPoint, MinimumAmountException minimumAmountException) { LOGGER.info("抛出异常: " + minimumAmountException.getMessage()); } @Around("doAccountTransaction()") public boolean aroundTransaction(ProceedingJoinPoint proceedingJoinPoint){ LOGGER.info("调用方法前 "); boolean isTransactionSuccessful = false; try { isTransactionSuccessful = (Boolean)proceedingJoinPoint.proceed(); } catch (Throwable e) { } LOGGER.info("调用方法后"); return isTransactionSuccessful; } }
上述AccountServiceAspect就是一个切面,代表了Spring AOP机制的典型使用方法,我们一一来展开讨论。
首先,我们看到这里使用@Pointcut注解定义了一个切点,并通过execution()指示器限定该切点匹配的包结构为com.springboot.aop.service,匹配的方法是AccountService类的doAccountTransaction()方法。也就是说,针对com.springboot.aop.service.AccountService类中doAccountTransaction()方法的任何一次调用,都会触发切面,也就会执行对应的通知逻辑。请注意,因为在Spring AOP中连接点只支持方法的调用,所以这里专门定义了一个doAccountTransaction()方法,并在该方法上使用了@Pointcut注解。
另外,在AccountServiceAspect中综合使用了Spring AOP所提供的@Before、@After、@Around、@AfterThrowing和@AfterReturning注解来设置五种不同类型的通知。
在使用这些通知注解时,同样需要注意它们的目标切点都是添加了@Pointcut注解的doAccountTransaction()方法。这些注解对应的实现方法都不复杂,这里不一一展开讨论。当执行AccountServiceImpl的doAccountTransaction()方法时,我们在控制台中能够看到如代码清单3-4所示的日志信息,这说明Spring AOP已经生效。
代码清单3-4 AOP输出日志代码示例
INFO AccountServiceAspect:50 - 调用方法前 INFO AccountServiceAspect:27 - 交易前 INFO AccountServiceImpl:14 - 执行交易 INFO AccountServiceAspect:32 - 交易后 INFO AccountServiceAspect:58 - 调用方法后
本案例的详细实现代码可以在以下地址下载:https://github.com/tianminzheng/spring-boot-examples/tree/main/SpringAopExample。