介绍完Spring AOP所具备的功能特性,接下来,让我们看看在应用程序中使用AOP时应该遵循哪些最佳实践。
Spring AOP的一大特色在于为开发人员提供了非常灵活的切点机制。Spring在编译期间处理切入点,并尝试进行优化匹配。然而,检查代码中的匹配规则将是一个代价高昂的过程。因此,为了获得最佳性能,我们需要仔细考虑想要实现的目标,并尽可能缩小搜索或匹配条件的范围。
我们在3.1.2节中已经看到过一个切点表达式,如代码清单3-21所示。
代码清单3-21 切点表达式代码
@Pointcut("execution(* com.springboot.aop.service.AccountService.doAccountTransaction(..))") public void doAccountTransaction() {}
这里的execution()代表的就是表达式的主体,它的基本语法如代码清单3-22所示,其中“?”部分表示可选项,可以为空。
代码清单3-22 execution()基本语法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
这个语法看似复杂,但是我们逐个分解所有的模式,它们其实就是描述了一个方法的特征。
这些模式的作用就是完成切点的匹配。在各个模式中,可以使用“*”来表示匹配所有选项。Spring AOP还为开发人员提供了一组非常有用的描述符来简化切点表达式的使用过程。例如,args描述符表示方法的参数属于一个特定的类;within描述符表示方法属于一个特定的类;target描述符表示方法所属的类等。关于这些描述符的具体使用方法,可以参考Spring AOP的官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.xhtml#spring-core。
为了获得良好的性能,在设计切点表达式时,至少应该包含方法和类型模式。这并不是说如果只使用方法或类型模式中的一种,匹配就会不生效,而是因为类型模式的匹配过程非常快,它通过快速选择无法进一步处理的连接点来缩小搜索空间。
同时,建议在空方法上声明切点,并通过空方法名引用这些切点。我们在3.1.2节中定义的doAccountTransaction()方法就是一个很好的空方法。基于这种定义,针对需要对切点表达式进行任何更改的场景,只需要修改一个位置即可。
另外一项最佳实践在于尽量声明小的切点,并把它们组合起来构建复杂的切点。代码清单3-23展示了定义小切点并将它们连接起来的代码示例。
代码清单3-23 定义并连接小切点代码示例
@Pointcut("execution(public * *(..))") private void anyPublicMethod() {} @Pointcut("execution(* com.springboot.aop.service.AccountService.doAccountTransaction(..))") public void doAccountTransaction() {} @Pointcut("anyPublicMethod() && doAccountTransaction()") private void transactionOperation() {}
这里的transactionOperation()就是由anyPublicMethod()和doAccountTransaction()这两个切点组合而成的。在日常开发过程中,我们可以根据需要定义各种粒度的切点,并把它们灵活地进行组合。
请注意,并不是所有场景下Spring AOP都是能够生效的,例如,在代码清单3-24所示的ServiceImpl中,直接调用添加了@Transactional注解的handleData()方法时,事务机制并不会生效。
代码清单3-24 在类内方法上使用代理代码示例
public class ServiceImpl implements Service { @Override public void performBusiness(){ //事务无效 this.handleData(); } @Transactional public void handleData() { } }
这是因为Spring AOP是通过代理实现的,而无论是JDK代理还是CGLIB代理,其运行机制是对某一个外部的接口或实现类进行代理,像上述代码中直接调用ServiceImpl类内的方法是不会应用代理的。
解决这一问题的常见方法就是使用上下文对象AopContext,示例代码如代码清单3-25所示。
代码清单3-25 AopContext使用代码示例
public class ServiceImpl implements Service { public void performBusiness(){ //从AopContext中获取代理对象 ((Service)AopContext.currentProxy()).handleData(); } @Transactional public void handleData() { } }
这里我们直接从AopContext中获取代理对象。当然,上述代码生效的前提是确保ProxyFactoryBean的exposeProxy属性被设置为true,正如我们在3.2.3节中讨论的那样。
不要在已经受Spring管理的Bean类上使用@Configurable注解,否则它将执行双重初始化,一次是通过Spring容器,一次是通过AOP切面。这是因为,@Configurable这个注解的作用就是告诉Spring在构造函数运行之前将依赖关系注入对象中。
Spring的推荐做法是尽可能使用JDK动态代理而不是CGLIB代理。如果从头开始构建应用程序,并且不需要创建对第三方API的代理,那么建议一切以面向接口的方式来驱动整个系统的设计过程。通过合理设计接口,我们可以实现业务的抽象层,从而确保系统的松耦合架构。这时,让Spring使用基于接口的JDK动态代理机制来创建代理。