写点什么

深入理解 Spring 核心容器面向切面概念和实践:AOP 实战经验

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

    阅读完需:约 10 分钟

前面给大家介绍了 Spring AOP 所具备的功能特性,接下来,让我们看看在应用程序中使用 AOP 时应该遵循哪些最佳实践。

活用切点表达式

Spring AOP 的一大特色在于为开发人员提供了非常灵活的切点机制。


Spring 在编译期间处理切入点,并尝试进行优化匹配。然而,检查代码中的匹配规则将是一个代价高昂的过程。


因此,为了获得最佳性能,我们需要仔细考虑想要实现的目标,并尽可能缩小搜索或匹配条件的范围。

我们在前面已经看到过一个切点表达式,代码如下所示:

@Pointcut("execution(*com.springboot.aop.service.AccountService.doAccountTransaction(..))")public void doAccountTransaction() {}
复制代码

这里的 execution()代表的就是表达式的主体,它的基本语法如下所示,其中“?”部分表示可选项,可以为空。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)
复制代码

这个语法看似复杂,但是我们逐个分解所有的模式,它们其实就是描述了一个方法的特征:

  • 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.html#spring-core。


为了获得良好的性能,在设计切点表达式时,至少应该包含方法和类型模式。这并不是说如果只使用方法或类型模式中的一种,匹配就会不生效,而是因为类型模式的匹配过程非常快,它通过快速选择无法进一步处理的连接点来缩小搜索空间。


同时,建议在空方法上声明切点,并通过空方法名引用这些切点。我们在前面定义的 doAccountTransaction()方法就是一个很好的空方法。基于这种定义,针对需要对切点表达式进行任何更改的场景,只需要修改一个位置即可。


另外一项最佳实践在于尽量声明小的切点,并把它们组合起来构建复杂的切点。下面展示了定义小切点并将它们连接起来的代码示例:

@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 都是能够生效的,例如,在如下所示的 ServiceImpl 中,直接调用添加了 @Transactional 注解的 handleData()方法时,事务机制并不会生效。

public class ServiceImpl implements Service {	@Override	public void performBusiness(){		//事务无效		this.handleData();	}	@Transactional	public void handleData() {	}}
复制代码

这是因为 Spring AOP 是通过代理实现的,而无论是 JDK 代理还是 CGLIB 代理,其运行机制是对某一个外部的接口或实现类进行代理,像上述代码中直接调用 ServiceImpl 类内的方法是不会应用代理的。

解决这一问题的常见方法就是使用上下文对象 AopContext,示例代码如下所示:

public class ServiceImpl implements Service {	public void performBusiness(){		//从AopContext中获取代理对象		((Service)AopContext.currentProxy()).handleData();	}	@Transactional	public void handleData() {	}}
复制代码

这里我们直接从 AopContext 中获取代理对象。当然,上述代码生效的前提是确保 ProxyFactoryBean 的 exposeProxy 属性被设置为 true,正如我们在前面讨论的那样。

避免代理机制引起多次初始化过程

不要在已经受 Spring 管理的 Bean 类上使用 @Configurable 注解,否则它将执行双重初始化,一次是通过 Spring 容器,一次是通过 AOP 切面。这是因为,@Configurable 这个注解的作用就是告诉 Spring 在构造函数运行之前将依赖关系注入对象中。

优先使用 JDK 动态代理

Spring 的推荐做法是尽可能使用 JDK 动态代理而不是 CGLIB 代理。如果从头开始构建应用程序,并且不需要创建对第三方 API 的代理,那么建议一切以面向接口的方式来驱动整个系统的设计过程。通过合理设计接口,我们可以实现业务的抽象层,从而确保系统的松耦合架构。这时,让 Spring 使用基于接口的 JDK 动态代理机制来创建代理。

Spring AOP 面试题分析

面试题 1:Spring AOP 是基于什么技术体系来实现的?

答案:Spring AOP 的实现依赖代理机制。代理机制在具体实现上一般有两种方式,一种是静态代理机制,一种是动态代理机制。Spring AOP 基于动态代理模式提供了面向切面机制。动态代理理解和实现起来比较复杂,我们专门通过一节内容对其进行了详细的阐述。而且动态代理机制的应用非常广泛,在 Dubbo、MyBatis 等框架中的应用方式和实现过程值得我们学习和模仿,同样这也是面试过程中经常会碰到的话题。


面试题 2:Spring AOP 中提供了哪些类型的通知机制?

答案:Spring AOP 的通知机制类型非常丰富,开发人员可以在方法执行之前、之后、前后、返回以及抛出异常时实现各种自定义的通知逻辑。而且,Spring AOP 实现通知的方式很简单,用一组注解即可,这些注解包括 @Before、@After、@Around、@AfterReturning 和 @AfterThrowing 等,分别对应于方法执行的各个阶段。


面试题 3:Spring AOP 使用了哪几种动态代理机制?性能上哪种更优?

答案:常见的动态代理实现技术包括 JDK 自带的代理类、第三方的 CGLIB 和 javassist。在回答该问题时,这三个名词是一定要点到的。至于具体的细节,可以视面试的进展合理进行展开,包括给出一些自己开发过程中的实践体会,或者部分核心类的介绍。


在 Spring AOP 中,采用了上述三种动态代理机制中的两种,即 JDK 和 CGLIB。从性能上讲,JDK 动态代理是优于 CGLIB 的,本章通过一个案例分析给出了这个结论。在面试过程中,可以把案例的设计方法和实现过程做一些展开。


面试题 4:如果想要在一个类的内部方法上实现 AOP,你有什么办法?

答案:关于 Spring AOP 有一点需要注意,我们只能在方法的调用过程中嵌入通知机制。这是很重要的一个限制,会导致对同一个类中的内部方法无法有效地实施动态代理。这时候,我们可以使用 Spring AOP 提供的 AopContext 上下文对象来获取当前的 AOP 代理。AopContext 是一个非常有用的工具类,想要获取该类,需要确保 ProxyFactoryBean 的 exposeProxy 属性被设置为 true。


面试题 5:如果想要基于 Spring AOP 实现对切点的精细化管理,你有什么策略?

答案:Spring AOP 创建切点的方式是非常灵活的,Spring 专门提供了一个 execution()配置方法。开发人员可以根据方法的修饰符、返回值、类路径、方法名、方法参数以及异常信息来设置方法调用与切点的匹配规则。这是实现精细化管理的一个维度。


第二个精细化管理的维度是设置切点的粒度。我们可以尽量声明小的切点,并把它们组合起来构建复杂的切点。这样,切点之间就具备了灵活的可重用性以及组合性。

用户头像

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

公众号:程序员高级码农

评论

发布
暂无评论
深入理解Spring核心容器面向切面概念和实践:AOP实战经验_互联网架构师小马_InfoQ写作社区