写点什么

3 年经验程序员终于把 Spring 核心容器的面向切面与 Spring AOP 搞懂了

  • 2023-06-17
    湖南
  • 本文字数:3385 字

    阅读完需:约 11 分钟

面向切面概念和实践

在软件开发领域,基于面向对象编程(Object Oriented Programming,OOP)的思想和实践,应用程序被划分为多个类和模块。我们通过引入接口来实现松散耦合的设计,而封装和继承使得我们可以隐藏对象数据并扩展功能。但反过来讲,随着系统的演进,OOP 的这些特点也增加了系统的复杂性。


为了解决这个问题,我们开始遵循将应用程序划分为不同逻辑层和模块的设计原则,常见的如 Web 服务层、业务服务层和数据访问层。


但是,即使将功能划分为不同的层,所有层中也可能需要某些通用的功能,例如安全性、日志记录、缓存和性能监视,这些功能被称为横切关注点(Crosscutting Concern)。在 OOP 中,这些横切关注点的代码与业务逻辑处理代码往往是混合在一起的,下图所示的就是一个典型的场景:

上图展示了在业务服务层组件的实现过程中,事务处理和安全控制代码与业务逻辑代码混合在了一起。这样的实现降低了代码的可重用性,增加了维护成本,并且违反了单一责任原则。


那么,如何有效实现这些横切关注点?这就需要引入面向切面编程(Aspect Oriented Programming,AOP)的设计理念。


在本篇中,我们将讨论 Spring 容器中 AOP 的概念以及实现这些概念的方法和实践。

面向切面与 Spring AOP

在引入 Spring AOP 之前,我们先来解释什么是切面。所谓切面,本质上解决的是关注点分离的问题。而面向切面编程可以说是面向对象编程的一种补充,目标是将一个应用程序抽象成各个切面。针对图 3-1 中所示的应用场景,可以引入 AOP 的思想把事务处理和安全控制等非功能性需求从业务逻辑中拆分出来,构成独立的关注点,如图所示:

从上图中可以很形象地看出,所谓切面相当于对象间的横切点,我们可以将其抽象为单独的模块进行开发和维护。

Spring AOP 核心概念

AOP 只是一种设计理念,虽然概念不复杂,但实现过程不那么简单。而 Spring 作为 AOP 的一款具体实现框架,也提供了自身的一些设计思想和编程组件。本小节将对 Spring AOP 中的核心概念展开讨论,为下一小节的使用示例提供基础。


为了理解 AOP 的具体实现过程,我们首先需要引入一组特定的术语,包括连接点(JoinPoint)、通知(Advice)、切点(PointCut)以及切面(Aspect)。


在 AOP 中,连接点表示应用执行过程中能够插入切面的一个点。这种连接点可以是方法调用、异常处理、类初始化或对象实例化。


所谓通知,描述的是切面何时执行以及如何执行对应的业务逻辑。通知有很多种类型,比方说在方法执行之前、之后、前后及执行完成时进行通知,或者在方法执行异常时进行通知。


请注意,通知不一定应用于所有连接点,所以我们引入了切点的概念。


切点是连接点的集合,用于定义必须执行的通知。因此切点为组件在应用程序中执行具体的通知提供了细粒度控制的方法。


最后,通知和切点组合在一起就构成了切面。切面用于定义应用程序中的业务逻辑及其执行的位置。


以上概念比较抽象,我们通过下图来使这些概念具象化:

Spring 框架对上图中的概念都进行了实现,但也有自身的一些限制。例如,连接点只支持方法的调用。针对通知,Spring 专门提供了一组注解,包括 @Before、@After、@Around、@AfterReturning 和 @AfterThrowing 等,分别对应方法执行的各个阶段。切点的定义是和业务流程执行紧密相关的,所以在 Spring 中,可以通过使用各种灵活的表达式来定义切点。最后,Spring 专门提供了一个 @Aspect 注解来定义切面。

Spring AOP 案例分析

在理解了 AOP 的相关概念以及 Spring 框架所提供的各种注解之后,在本节中,我们将通过代码示例来展示注解的使用方法。

现在,假设有一个代表用户账户的 AccountService 接口,如下所示:

public interface AccountService {	boolean doAccountTransaction(Account source, Account dest, int amount) throws MinimumAmountException;}
复制代码

可以看到,在该 AccountService 接口中定义了一个用于实现账户交易的 doAccount-Transaction()方法。然后我们提供它的实现类,如下所示:

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,其实现过程如下所示:

@Aspectpublic 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 注解来设置五种不同类型的通知:

  • @Before:在方法调用之前调用通知。

  • @After:在方法完成之后调用通知,无论方法执行成功与否。

  • @Around:通知包裹了目标方法,在被通知的方法调用之前和调用之后执行自定义的行为。

  • @AfterThrowing:在方法抛出异常后进行通知,可以通过该注解指定目标异常信息。

  • @AfterReturning:在方法执行成功之后调用通知。


在使用这些通知注解时,同样需要注意它们的目标切点都是添加了 @Pointcut 注解的 doAccountTransaction()方法。这些注解对应的实现方法都不复杂,这里不一一展开讨论。当执行 AccountServiceImpl 的 doAccountTransaction()方法时,我们在控制台中能够看到如下所示的日志信息,这说明 Spring AOP 已经生效。

INFO AccountServiceAspect:50 - 调用方法前INFO AccountServiceAspect:27 - 交易前INFO AccountServiceImpl:14 - 执行交易INFO AccountServiceAspect:32 - 交易后INFO AccountServiceAspect:58 - 调用方法后
复制代码


用户头像

加VX:bjmsb02 凭截图即可获取 2020-06-14 加入

公众号:程序员高级码农

评论

发布
暂无评论
3年经验程序员终于把Spring核心容器的面向切面与Spring AOP搞懂了_互联网架构师小马_InfoQ写作社区