1 概述
在软件开发领域,基于面向对象编程(OOP)的思想和实践,应用程序被划分为多个类和模块。通过引入接口来实现松散耦合的设计,而封装和继承使得我们可以隐藏对象数据并扩展功能。但反过来讲,随着系统的演进,OOP的这些特点也增加了系统的复杂性,为了解决这个问题,我们开始遵循将应用程序划分为不同逻辑层和模块的设计原则,常见的如Web服务层、业务服务层和数据访问层。
但是,即使将功能划分为不同的层,所有层中也可能需要某些通用的功能,例如安全性、日志记录、缓存和性能监视。这些功能被称为横切关注点。在OOP中,这些横切关注点的代码与业务逻辑处理代码往往是混合在一起的。例如,事务处理和安全控制代码与业务逻辑代码混合在一起,这样的实现奖励了代码的可重用型性,增加了维护成本,并且违反单一责任原则。
那么,如何有效实现这些横切关注点?这就需要引入面向切面编程的设计理念。本篇将导论Spring容器中AOP的概念以及实现这些概念的方法和实践。
2 面向切面与Spring AOP
在引入Spring AOP之前,先来解释什么是切面。所谓切面,本质上解决的是关注点分离的问题。而面向切面编程可以说是面向对象编程的一种补充,目标是将一个应用程序抽象成各个切面。针对下图所示的应用场景中,可以引入AOP的思想把书屋处理和安全控制等非功能性需求从业务逻辑中拆分出来,构成独立的关注点。
从上图中可以看出,所谓切面相当于对象间的横切点,可以将其抽象为单独的模块进行开发和维护。
3 Spring AOP核心概念
AOP只是一种设计理念,虽然概念不复杂,但实现过程不那么简单。而Spring作为AOP的一款具体实现框架,也提供了自身的一些设计思想和编程组件。
为了理解AOP的具体实现过程,首先需要引入一组特定的术语,包括连接点(JoinPoint)、通知(Advice)、切点(PointCut)以及切面(Aspect)。
在AOP中,连接点表示应用执行过程中能够插入切面的一个点。这种连接点可以是方法调用、异常处理、类初始化或对象实例化。
通知,描述的是切面何时执行以及如何执行对应的业务逻辑。通知有很多种类型,比如在方法执行之前、之后、前后和执行完成时进行通知,或者在方法执行异常时进行通知。
请注意,通知不一定应用于所有连接点,所以引入了切点的概念。切点是连接点的集合,用于定义必须执行的通知。因此切点为组件在应用程序中执行具体的通知提供了细粒度控制的方法。
最后,通知和切点组合在一起就构成了切面。切面用于定义应用程序中的业务逻辑及其执行的位置。
以上概念比较抽象,下面通过图来使这些概念具象化,如下图:
public interface AccountService {
boolean doAccountTransaction(Account source, Account dest, int amount) throws MinimumAccountException;
}
Spring框架对上图的概念都进行了实现,但也有自身的一些限制。例如,连接点只支持方法 的调用。针对通知,Spring专门提供了一组注解,包括@Before、@After、@Around、@AfterReturning和@AfterThrowing等,分别对应方法执行的各个阶段。切点的定义是和业务流程执行紧密相关的,所以在Spring中,可以通过使用各种灵活的表达式来定义切点。最后,Spring专门提供了一个@Aspect注解来定义切面。
4 Spring AOP案例分析
在理解了AOP的相关概念以及Spring框架所提供的各种注解之后,下面将通过代码来展示注解的使用方法。
现在,假设有一个代表用户账户的AccountService接口,代码如下:
public interface AccountService {
boolean doAccountTransaction(Account source, Account dest, int amount) throws MinimumAccountException;
}
可以看到,在该AccountService接口中定义了一个用于实现账户交易的 doAccountTransaction方法。下面定义模型Account类和异常MinimumAccountException类。
public class Account {
private String accountName;
private Integer accountNumber;
public Account(String accountName, Integer accountNumber) {
this.accountName = accountName;
this.accountNumber = accountNumber;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public Integer getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(Integer accountNumber) {
this.accountNumber = accountNumber;
}
}
public class MinimumAccountException extends Exception{
private static final long serialVersionUID = 1L;
public MinimumAccountException(String message) {
super(message);
}
}
然后提供 AccountService.java的实现类。
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 MinimumAccountException {
LOGGER.info("执行交易");
if(amount < 10){
throw new MinimumAccountException("交易金额过少");
}
return true;
}
}
在doAccountTransaction()方法中,在执行交易之前记录了操作日志,这种实现方式看上去没有什么问题。如果针对交易操作,希望在该操作之前、之后、执行过程中以及抛出异常时都记录对应的日志,那么实现起来就没有那么容易了。这个时候,可以通过AOP进行切入,并添加对应的日志操作。基于Spring AOP,实现过程如下:
@Aspect
public class AccountServiceAspect {
private static final Logger LOGGER = Logger.getLogger(AccountServiceAspect.class);
@Pointcut("execution(* com.jay.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() && 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 = "minimumAccountException")
public void excetionFromTransaction(JoinPoint joinPoint, MinimumAccountException minimumAccountException){
LOGGER.info("抛出异常:" + minimumAccountException.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;
}
}
上述的AccoutServiceAspect就是一个切面,代表了Spring AOP机制的典型使用方法。
首先,看到这里使用@Pointcut注解定义了一个切点,并通过execution()指示器限定该切点匹配的包结构为com.jay.aop.service,匹配的方法是AccountService类的doAccountTransaction()方法。也就是说,针对com.jay.aop.service.AccountService类中的doAccountTransaction方法的任何一次调用,都会触发切面,也就会执行对应的通知逻辑。请注意,因为在Spring AOP中连接点只支持方法的调用,所以这里专门定义了一个doAccountTransaction方法,并在该方法上使用了@Pointcut注解。
另外,在AccoutServiceAspect类中综合使用了Spring AOP所提供的@Before、@After、@Around、@AfterThrowing和@AfterReturning注解来设置物种不同类型的通知。
1、@Before:在方法调用前调用通知。
2、@After:在方法完成之后调用通知,无论方法执行成功与否。
3、@Around:通知包裹了目标方法,在被通知的方法调用之前和调用之后执行自定义的行为。
4、AfterThrowing:在方法抛出异常后进行通知,可以通过该注解指定目标异常信息。
5、@AfterReturning:在方法执行成功之后调用通知。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Appcofig.class)
public class AopTest {
@Autowired
private AccountService accountService;
public void transferAmount() throws MinimumAccountException{
Account source = new Account("Account1",123456);
Account dest = new Account("Account2",987654);
accountService.doAccountTransaction(source,dest,100);
}
public void transferAmountException() throws MinimumAccountException{
Account source = new Account("Account1",123456);
Account dest = new Account("Account2",987654);
accountService.doAccountTransaction(source,dest,9);
}
}
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
@Bean
public AccountService transferService(){
return new AccountServiceImpl();
}
@Bean
public AccountServiceAspect transferServiceAspect(){
return new AccountServiceAspect();
}
}