面向切面的 Spring
本章主要内容:
面向切面编程的基本原理
通过 POJO 创建切面
使用 @Aspect 注解
为 AspectJ 切面注入依赖
说明
如果你有幸能看到
1 本文参考了 Spring 实战重点内容, 参考了 GitHub 上的代码
2 本文只为记录作为以后参考, 要想真正领悟 Spring 的强大, 请看原书
3 在一次佩服老外, 国外翻译过来的书, 在 GiuHub 上大都有实例看书的时候, 跟着敲一遍, 效果很好
4 代码和笔记在这里 GitHub, 对你有帮助的话, 欢迎点赞
5 每个人的学习方式不一样, 找到合适自己的就行 2018, 加油
6 问候了下 Java 8 In Action 的作者 Mario Fusco, 居然回复了
7Spring In Action Spring Boot In Action 的作者 Craig Walls 老忙了, 没理睬
8 知其然, 也要知其所以然
谈一些个人感受
1 赶快学习 Spring 吧, Spring MVC Spring Boot 微服务
2 重点中的重点, 学习 JDK 8 Lambda,Stream,Spring 5 最低要求 JDK1.8.
3 还有 Netty 放弃 SH 吧, 不然你会落伍的
4 多看一些国外翻译过来的书, 例如 Xxx In Action 系列权威指南系列用 Kindle~
软件系统中的一些功能就像我们家里的电表一样则核心功能需要用到应用程序的多个地方但是我们又不想在每个点都明确调用它日志安全事务管理的确很重要但它们是否为应用对象主动参与的行为呢? 如果让应用对象只关注与自己所针对的业务领域问题, 而其他方面的问题由其他应用对象来处理, 这样不更好吗?
在软件开发中, 散布于应用中多出功能被称为横切关注点 (crosscutting concern) 通常来讲横切关注点从概念上是与应用的业务逻辑分离的但往往是耦合在一起的, 把这些横切关注点与业务逻辑相分离正是面向切面编程 (AOP) 所要解决的问题
依赖注入 (DI) 管理我们的应用对象, DI 有助于应用对象之间解耦而 AOP 可以实现横切关注点与它们所影响的对象之间的耦合
4.1 什么是面向切面编程
切面能够帮我们模块化横切关注点简而言之, 横切关注点可以被描述为影响应用多处的功能例如 安全, 事务日志等功能
如果要重用对象的话, 最常见的面向对象技术是继承委托组合但是, 如果整个应用中都使用相同的基类, 继承往往会导致一个脆弱的对象体系而使用委托可能需要委托对象进行复杂的调用
切面提供了取代继承和委托的另一种可选方案在使用面向切面编程时, 我们仍然在一个地方定义通知功能, 而无需修改受影响的类横切关注点可以被模块化为特殊的类, 这些类被称为切面 (aspect). 这样做带来两个好处: 每个关注点都集中到一个地方, 而不是分散到多处代码中: 其次, 服务模块更简洁, 因为它只包含了主要关注点(核心功能) 的代码而次要关注的代码被移到切面中了
4.1.1 定义 AOP 术语
描述切面的常用术语有: 通知 (advice) 切点(pointcut)(连接点)
通知(advice)
通知定义了切面是什么以及何时使用除了描述切面要完成的工作外, 通知还解决了何时执行这个工作问题它应该在某个方法被调用之前? 之后? 之前和之后都调用? 还是只在方法抛出异常时调用?
Spring 切面可以应用 5 中类型的通知:
前置通知(Before): 在目标方法被调用之前调用通知功能
后置通知(After): 在目标方法完成之后调用通知
返回通知(After-returning): 在目标方法成功执行之后调用通知
异常通知(After-throwing): 在目标方法抛出异常后调用通知
环绕通知(Around): 在被通知方法调用之前和调用之后执行自定义的行为
连接点
我们的应用可能有数以千计的时机应用通知, 这些时机被称为连接点连接点是在应用执行过程中能够插入的一个点这个点可以是调用方法时, 抛出异常时, 甚至修改一个字段时切面可以利用这些点插入到应用的正常流程之中, 并添加新的行为
切点 如果说通知定义了切面的的什么和何时, 那么切点定义了何处切点的定义会匹配通知所要织入的一个或多个连接点
切面 切面是通知和切点的结合通知和切点通过定义了切面的全部 内容他是什么, 在什么时候和在哪里完成其功能
引入 引入允许我们向现有的类添加新的方法或者属性
织入
织入是把切面应用到目标对象并创建新的代理对象的过程切面在指定的连接点被织入到目标对象在目标对象的生命周期里有多个点可以进行织入:
编译器: 切面在目标类编译时被织入 Aspect 的织入编译器就是以这种方式织入切面的
类加载器: 切面在目标类加载到 JVM 时被织入需要特殊的类加载(Classloader), 它可以在目标类被引入之前增强该目标类的字节码(CGlib)
运行期: 切面在应用运行时的某个时刻被织入 AOP 会为目标对象创建一个代理对象
通知包含了需要用于多个应用对象的横切关注点连接点是程序执行过程中能够应用通知的所有点切点定义了通知被应用的具体位置(在哪些连接点), 其中关键是切点定义了哪些连接点会得到通知
4.1.2 Spring 对 AOP 的支持
并不是所有的 AOP 框架都是相同的, 他们在连接点模型上可能有强弱之分有些允许在字段修饰符级别的通知, 而另一些只支持与方法调用相关的连接点它们织入切面的方式和时机也有所不同但是, 无论如何, 创建切点来定义切面所织入的连接点是 AOP 的基本功能
Spring 提供了 4 种类型的 AOP 支持:
基于代理的经典 Spring AOP
纯 POJO 切面
@AspectJ 注解驱动的切面
注入式 AspectJ 切面
前三种都是 Spirng AOP 实现的变体, Spring AOP 构建在动态代理基础上因此, Spring 对 AOP 的支持局限于方法拦截
引入了简单的声明式 AOP 与基于注解的 AOP 之后, Spring 经典的看起来就显得非常笨拙和过于复杂话, 直接使用 ProxyFactory bean 会让人感觉厌烦
借助于 Spring 的 aop 命名空间, 我们可以将纯 POJO 转为切面
Spring 借鉴了 AspectJ 的切面, 以提供注解驱动的 AOP 本质上, 它依然是 Spring 基于代理的 AOP, 但是编程模型几乎与编写成熟的 AspectJ 注解切面完全一致这种 AOP 风格的好处在于能够不使用 XML 来完成功能
Spring 所创建的通知都是用标准的 Java 类编写的, 定义通知所应用的切点通常会使用注解或在 Spring 配置文件里采用 XML 来编写
通知带代理类中包裹切面, Spring 在运行时把切面织入到 Spring 所管理的 bean 中代理类封装了目标类, 并拦截被通知方法的调用再把调用转发给真正的目标 bean 当代理拦截到方法调用时, 在调用目标 bean 方法之前, 会执行切面逻辑直到应用需要被代理 bean 时, Spring 才会创建代理对象如果使用 ApplicationContext 的话, 在 ApplicationContext 从 BeanFactory 中加载所有的 bean 的时候, Spring 才会创建被代理的对象因为 Spirng 运行时才创建代理对象, 所以我们不需要特殊的编译器来织入 Spring AOP 的切面
Spring 基于动态代理, 所以 Spring 只支持方法连接点方便拦截可以满足大部分的需求
4.2 通过切点来选择连接点
切点用于准确定位应该在什么地方应用切面的通知通知和切点是切面最基本的元素
Spring 仅支持 AspectJ 切点指示器的一个子集 Spring 是基于代理的, 而某些切点表达式是基于代理的 AOP 无关的
Spring 支持的指示器, 只有 execution 指示器是实际执行匹配的, 而其他的指示器都是用来限制匹配的这说明 execution 指示器是我们在编写切点定义时最主要的指示器
4.2.1 编写切点
为了阐述 Spring 中的切面, 我们需要有个主题来定义切面的切点
- package com.guo.cocert;
- public interface Performance {
- public void perform();
- }
- execution(* concert.Performance.perform(..))
我们使用 execution()指示器选择 Performance 的 perform()方法, 方法表达式以 "*" 号开始, 表明了我们不关心方法返回值的类型然后指明了全限定类名和方法名, 对于方法参数列表, 我们使用了两个点号 (..) 表明切点要选择任意的 perform()方法, 无论该方法的入参是什么
现在假设我们需要配置的切点仅匹配 concert 包, 可以使用 within()指示器
execution(* concert.Performance.perform(..)) && within(concert.*)
因为 & 在 XMl 中有特殊的含义, 所以在 Spring 和 XML 配置中, 描述切点时, 可以使用 and 代替 &&
4.2.2 在切点中选择 bean
Spring 引入了一个新的 bean()指示器, 它允许我们在切点表达式中使用 bean 的 ID 来标识 beanbean()使用 bean ID 或 bean 名称作为参数来限制切点只匹配特定的 bean
execution(* concert.Performance.perform(..)) and bean("woodsotck")
也可以这样
execution(* concert.Performance.perform(..)) and !bean("woodsotck")
切面的通知会被编织到所有 ID 不为 woodsotck 的 bean 中
4.3 使用注解创建切面
使用注解来创建切面是 AspectJ 5 所引入的关键特性
4.3.1 定义切面
如果一场演出没有观众的话, 那不能称之为演出
- @AspectJ
- public class Audience {
- }
Audience 类使用 @AspectJ 注解进行了标注该注解表明 Audience 不仅仅是一个 POJO, 还是一个切面 Audience 类中的方法都是使用注解来定义切面的具体行为
- @AspectJ
- public class Audience {
- @Pointcut("execution(* * concern.Performance.perform(..))")
- public void performance() {};
- }
在 Autience 中, performance()方法使用了 @Pointcut 注解为 @Pointcut 注解设置的值是一个切点表达式, 就像之前在通知注解上所设置的那样
需要注意的是, 除了注解和没有实际操作的 performa()方法, Audience 类依然是一个 POJO, 我们能够像使用其他的 Java 类那样调用它的方法, 它的方法也能独立的进行单元测试与其他 Java 类没有什么区别
像其他的 Java 类一样, 它可以装配为 Spring 中的 bean
- @Bean
- public Audience audience() {
- return new Audience();
- }
如果你就此止步的话, Audience 只会是 Spring 容器中的一个 bean 即便使用了 AspectJ 注解, 但它并不会被视为切面, 这些注解不会解析, 也不会创建将其转化为切面的代理
如果你使用 JavaConfig 的话, 可以在配置类的级别上通过使用
EnableAspectJ-AutoProxy
注解启用自动代理功能
- @Configuration
- @EnableAspectJAutoProxy // 启用 AspectJ 自动代理
- @ComponentScan
- public class ConcertConfig {
- @Bean
- public Audience autidence() { // 声明 Audience bean
- return new Audience();
- }
- }
假如你在 Spring 中使用 XMl 来装配 bean 的话, 那么需要使用 Spring aop 命名空间中的 aop:aspect-autoproxy 元素
- <?xml version="1.0" encoding="UTF-8"?>
- <context:component-scan base-package="com.guo.concert"/>
- <aop:aspect-autoproxy/>
- <bean class="com.guo.concert.Audience"/>
不管你使用 JavaConfig 还是 XML,AspecJ 自动代理都会使用 @Aspect 注解的 bean 创建一个代理这个代理会围绕着所有该切面的切点所匹配的 bean
我们需要记住的是, Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导, 切面依然是基于代理的本质上它依然是 Spring 基于代理的切面
4.3.2 创建环绕通知
环绕通知是最为强大的通知类型, 它能够让你编写的逻辑将被通知的目标方法安全包装起来, 实际上就像在一个通知方法中同时编写前置通知和后置通知
- @AspectJ
- public class Audience {
- @Pointcut("execution(* * concern.Performance.perform(..))")
- public void performance() {};
- @Around
- public void xx(Xxx jp) {
- .......
- jp.proced()
- }
- }
在这里,@Around 注解, 表明这个 xx()方法会作为 performance()切点的环绕通知
这个通知所达到的效果与之前的前置通知和后置通知是一样的
需要注意的是, 别忘记调用 proceed()方法, 如果不调用这个方法, 那么你的通知实际上会阻塞对被通知方法的调用, 有意思的是, 你可以不调用 proceed 方法, 从而阻塞对被通知方法的反问,
4.3.4 通过注解引入新功能
一些编程语言, 例如: Ruby 和 Groovy, 有开放来的理念, 它们可以不直接使用修改对象或类的定义就能够为对象或类增加新的方法不过 Java 并不是动态语言, 一旦编译完成了, 就很难在为该类添加新的功能了
如果切面能够为现有的方法增加额外的功能, 为什么不恩那个为一个对象增加新的方法呢? 利用引入 AOP 的概念, 切面可以为 Spring bean 添加新的方法
在 Spring 中, 注解和自动代理提供了一种便利的方式来创建切面, 它非常简单, 并且只设计最少的 Spring 配置, 但是, 面向注解的切面有一个明显的不足点: 你必须能够为通知类添加注解, 为了做到这一点, 必须要有源码
4.4 在 XML 中声明切面
之前, 有这样一条原则: 那就是基于注解的配置要优于 Java 的配置, 基于 Java 的配置要优于 XMl 的配置, 但是, 如果你需要声明切面, 但是又不能为通知类添加注解的时候 , 那么就必须转向 XML 配置了
在 Spring 的 aop 命名空间中, 提供了多个元素用来在 XML 中声明切面,
aop:advisor : 定义 AOP 通知器
aop:after : 定义 AOP 后置通知
aop:after-returning : 定义 AOP 返回通知
aop:after-throwing : 定义 AOP 异常通知
aop:around : 定义 AOP 环绕通知
aop:aspect : 定义一个切面
aop:aspectj-autoproxy : 启用 @AspectJ 注解
aop:before : 定义一个 AOP 前置通知
aop:poiontcut : 定义一个切点
4.4.1 声明前置通知和后置通知
我们会使用 Spring aop 命名空间中的一些元素, 将没有注解的 Aurience 类转为切面
- <aop:config>
- <aop:aspect ref="audience"> <!-- 引用 audience Bean-->
- <aop:before pointcut="execution(* * concert.Performance.perform(..))" method="silenceCellIphones"/>
- <aop:before pointcut="execution(* * concert.Performance.perform(..))" method="takeSeats"/>
- <aop:after-returning pointcut="execution(* * concert.Performance.perform(..))" method="applause"/>
- <aop:after-throwing pointcut="execution(* * concert.Performance.perform(..))" method="demandRefund"/>
- </aop:aspect>
- </aop:config>
第一需要注意的就是大多数 AOP 配置元素必须在 aop:config 元素的上下文中使用
在所有的通知元素中, pointcut 属性定义了通知所应用的切点, 它的值是使用 AspectJ 切点表达式语法所定义的切点
在基于 Aspectj 注解的通知中, 当发现在这些类型的重复时, 使用 @Pointcut 注解来消除这些重复的内容
如下的 XMl 配置展示了如何将通用的切点表达式抽取到一个切点声明中, 这样, 这个声明就能在所有的通知元素中使用了
- <aop:config>
- <aop:aspect ref="audience"> <!-- 引用 audience Bean-->
- <aop:pointcut id="performance" expression="execution(* * concert.Performance.perform(..))" />
- <aop:before pointcut=""method="silenceCellIphones"/>
- <aop:before pointcut-ref="performance" method="takeSeats"/>
- <aop:after-returning pointcut-ref="performance" method="applause"/>
- <aop:after-throwing pointcut-ref="performance" method="demandRefund"/>
- </aop:aspect>
- </aop:config>
现在的切点是一个地方定义的, 并且被多个通知元素所引用, aop:pointcut 元素定义了一个 id 为 performance 的切点, 同时修改所有的通知元素, 用 pointcut0ref 来引用这个命名切点
4.4.2 声明环绕通知
相比于前置通知和后置通知, 环绕通知在这点上有明显的优势使用环绕通知, 我们可以完成前置通知和后置通知所实现的相同功能, 而且只需要在一个方法中实现因为整个通知逻辑都是在一个方法中实现的
- <aop:config>
- <aop:aspect ref="audience"> <!-- 引用 audience Bean-->
- <aop:pointcut id="performance" expression="execution(* * concert.Performance.perform(..))" />
- <aop:around pointcut-ref="performance" method="watchPerformance"/>
- </aop:aspect>
- </aop:config>
像其他通知的 XML 元素一样, aop:around 指定了一个切点和一个通知方法的名字
4.4.3 为通知传递参数
区别在于切点表达式中包含了一个参数, 这个参数传递到通知方法中还有区别就是这里使用了 and 关键字
4.4.4 通过切面引入新的功能
借助于 AspectJ 的 @DeclareParents 注解为被通知的方法引入新的方法但是 AOP 引入并不是 Aspectj 特有的使用 Spring aop 命名空间中的 aop:declare-parents 元素, 我们可以实现相同的功能
- <aop:config>
- <aop:aspect ref="audience"> <!-- 引用 audience Bean-->
- <aop:declare-parents types-matching="concert.Performance"
- implement-interface="concert.Encoreable"
- default-impl="concert.DefaoultEncoreable"
- </aop:aspect>
- </aop:config>
4.5 注入 AspectJ 切面
虽然 Spring AOP 能够满足许多应用的切面需求, 但是与 AspectJ 相比, Spring AOP 是一个功能比较弱的 AOP 解决方案, ASpect 提供了 Spring AOP 所不能支持的许多类型的切点
Spring 不能像之前那样使用声明来创建一个实例 ---- 它已经在运行时由 AspectJ 创建完成了, Spring 需要通过工厂方法获取切面的引用然后像元素规定的那样在该对象上执行依赖注入
4.6 小节(重点中的重点)
AOP 是面向对象编程的一个强大补充, 通过 AspectJ, 我们现在可以把之前分散在应用各处的行为放入可重用的模块中我们显示地声明在何处如何应用该行为这样有效减少了代码冗余, 并让我们的类关注自身的主要功能
Spring 提供了一个 AOP 框架, 让我们把切面插入到方法执行的周围现在我们已经学会了如何把通知织入前置, 后置和环绕方法的调用中, 以及为处理异常增加自定义行为
关于在 Spirng 应用中如何使用切面 , 我们可以有多种选择通过使用 @AspectJ 注解和简化的配置命名空间, 在 Spring 中装配通知和切点变得非常简单
最后, 当 Spring 不能满足需求时, 我们必须转向更为强大的 AspectJ 对于这些场景, 我们了解了如何使用 Spring 为 AspectJ 切面注入依赖
此时此刻, 我们已经覆盖了 Spring 框架的基础知识, 了解到如何配置 Spring 容器以及如何为 Spring 管理的对象应用切面, 这些技术为创建高内聚, 低耦合的应用奠定了坚实的基础
从下一章开始, 首先看到的是如何使用 Spring 构建 web 应用
期待......
来源: https://juejin.im/post/5a8fffd26fb9a063485375bc