技术的演化从来都不是随机现象。往往都是为了应对某种特定的问题,而形成的一系列切实可行解决方案或者优雅的最佳实践,然后把它们汇聚在一起,就形成了一个工具,一个库或者是一个框架。
要了解AOP(Aspect Oriented Programming,面向切面编程)从何而来,首先来看看下面这段代码:
public void doBusinessLogic() {
logger.trace("进入 " + CLASS_NAME + "." + METHOD_NAME); TransactionStatus tx = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 开始执行业务逻辑
// ......
// 业务逻辑结束
} catch (Exception e) {
logger.error("异常 " + CLASS_NAME + "." + METHOD_NAME, e);
tx.setRollbackOnly();
throw e;
} finally {
transactionManager.commit(tx);
logger.trace("退出 " + CLASS_NAME + "." + METHOD_NAME);
}
}
发现上面这段代码有什么问题了吗?很明显,样板代码(Boilerplate)太多了,真正重要的业务逻辑反而只占了很小的一部分(如果执行的业务逻辑比较简单的话)。这种代码结构合理吗?显然不合理。上述的样板代码还只包含了简单的Tracing,Transaction以及Exception处理,如果还需要更多的这类处理,那么代码将臃肿不堪。
所以,为了处理这类公用需求,最大程度地避免重复代码而遵循DRY原则。AOP应运而生。那么AOP要解决一个什么问题呢?
上面这张图能够说明要解决的问题。
每种颜色的代码就相当于公用的需求点,比如绿色的Tracing,蓝色的Transaction以及红色的Exception Handling。这些代码存在于不止一个类中,通常而言是分散的到处都是,就像上面的那段代码一样。
而这些重复而通用的代码就是AOP要解决的问题,每一个共同的横切关注点(Cross-cutting Concern)对应于一个切面(Aspect)。这些切面的目的就是将四处散落的通用代码集中管理,然后采用声明式的方式将这些代码再注入到需要执行的位置。
Aspect, Advice以及Pointcut的关系。
那么,什么是切面(Aspect)呢?
回顾一下上面的那张示意图。里面表达了切面的两个要素:
那么反映到AOP的概念中,这两个要素分别对应的是Advice以及Pointcut。
所以,简而言之:Aspect = Advice(做什么) + Pointcut(在哪做)
比如对于上面的Tracing功能而言:
@Component
@Aspect
public class TracingAspect {
private Logger logger = LoggerFactory.getLogger(TracingAspect.class);
@Before("execution(* *(..))")
public void beforeLogging(JoinPoint joinPoint) {
logger.trace("进入 "
+ joinPoint.getStaticPart().getSignature().toString());
}
}
这段代码实现了一个在执行方法时使用日志记录Tracing信息的切面(Aspect)。它的逻辑很清晰:
execution(* *(..))
来表达Pointcut的概念下面我们来看看如何在工程中启用AOP。
在一个传统的Spring项目中,可以采用XML或者Java Config的方式来开启对于AOP的支持:
<beans>
<aop:aspectj-autoproxy />
<context:component-scan base-package="com.destiny1020" />
beans>
的功能是开启对于@Aspect的支持。
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages="com.destiny1020")
public class AOPConfiguration {
}
上述的@EnableAspectJAutoProxy
就对应着XML配置中的
Advice的类型有5种:
简单介绍如下:
上面的例子中使用的就是Before Advice。它使用@Before注解来表达。
对于这类Advice,有几个注意事项:
一个简单的例子:
@After("execution(* *(..))")
public void afterLogging(JoinPoint joinPoint) {
logger.trace("退出 " + joinPoint.getSignature());
// 获取调用目标方法时的参数
for (Object arg : joinPoint.getArgs()) {
logger.trace("参数 : " + arg);
}
}
关于Pointcut表达式execution(* *(..))
会在后面Pointcut一节中进行介绍。
对于After Advice,有几个注意事项:
Around Advice是功能最强大的一种Advice。
下面是一例:
@Around("execution(* *(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
String minfo =
pjp.getStaticPart().getSignature().toString();
logger.trace("进入 " + minfo);
try {
return pjp.proceed();
} catch (Throwable ex) {
logger.error("异常 " + minfo, ex);
throw ex;
} finally {
logger.trace("退出 " + minfo);
}
}
如上面的图片所示,Around Advice可以被看成目标方法的一个Wrapper。它能够灵活地控制何时调用甚至不调用原本的目标方法。它有以下几个特点和注意事项:
ProceedingJoinPoint
作为参数,通过调用它的proceed方法来调用目标方法因此,Around Advice是所有Advice中功能最强大的一个。更灵活与更强大也就意味着更大的责任,它的使用也确实也相对复杂一些。在应用它的时候需要仔细调试确保能够满足所有的业务需求且不产生副作用。
一个例子:
@AfterReturning(pointcut = "execution(* *(..))", returning = "result")
public void returnLogging(String result) {
logger.trace("结果 "+ result);
}
值得注意的地方有:
一个例子:
@AfterThrowing(pointcut = "execution(* *(..))", throwing = "iae")
public void throwLogging(IllegalArgumentException iae) {
logger.error("非法参数异常 ", iae);
}
值得注意的地方有:
拿上面一直出现的execution(* *(..))
作为例子:
所以,Pointcut有几个重要的组成部分:
* *(..)
举几个例子:
execution(* register())
- 匹配所有不接受参数,名为register的方法,不限返回类型execution(int register(int, int))
- 匹配接受两个int类型作为参数,名为register的方法,且返回类型为int类型execution(* register(*))
- 匹配接受1个不限定参数类型,名为register的方法,不限返回类型execution(* com.destiny1020.AuthService.register(..)
- 匹配com.destiny1020.AuthService
类下的所有register重载execution(* com.destiny1020..*AuthService.register(..)
- 匹配com.destiny1020
子包下的类名以AuthService结尾的所有类中的register重载另外,还可以利用注解来限定Pointcut的范围。这个特性其实我们用得最多,诸如@Transactional
,@Cacheable
等等都可以归为这一类。那么如何在Pointcut表达式中进行声明呢,实际上可以分为两种情况:
execution(@com.destiny1020.anno.LoadBalanced * *(..))
execution(* (@com.destiny1020.anno.LoadBalanced *).*(..))
也就是说,一旦方法或者类被指定的注解给标注了,那么该方法或者类中的所有方法都会被定义为Advice的目标方法。需要注意的是,注解的名称需要是带有完整包名的限定名。
在Pointcut表达式中还能够使用逻辑操作符:
比如这个表达式:
execution(* com.destiny1020.service..*.load(..)) || execution(* com.destiny1020.repository..*.load(..))
它的意思很直观,即匹配service包或者repository包下的所有名为load的方法及其重载,不考虑参数数量和类型,也不考虑返回类型。
在实际的工作中可能会出现重复编写Pointcut的情况,与其到处粘贴复制,有没有一种方法能够仅仅定义一次呢,答案是可以通过@Pointcut
注解来帮我们:
public class PointcutDefinitions {
@Pointcut("execution(@com.destiny1020.anno.LoadBalanced * *(..))")
public void loadBalancedAnnotated() {
// 空方法就OK
}
}
然后在Advice的注解中使用即可:
@Around("PointcutDefinitions.loadBalancedAnnotated()")
public void trace(ProceedingJoinPoint pjp) throws Throwable {
// 定义Advice的逻辑
}
这样就可以将所有的Pointcut集中定义,不会把复杂而难以理解(至少乍一眼看上去是如此)的Pointcut弄的到处都是而难以维护了。
在下一篇文章中,会探讨AOP的两种实现:
Spring AOP Reference
AspectJ Quick Reference