AOP —> 面向切面编程
想要学习AOP思想,我们必须要理解几个名词:
横切关注点: 分布于应用中的众多功能被称为横切关注点。将横切关注点与业务逻辑相分离正是面向切面编程(AOP),横切关注点可以被模块化为特殊的类,这些类被称为切面。
通知(Advice): 定义了切面是什么以及何时使用切面。
连接点(Joinpoint): 定义了应用被通知的时机。
切点(Poincut): 定义了切面在何处使用。
切面(Aspect): 通知和切点共同定义了切面的全部内容(知道完成工作的一切事宜)。
引入(Introduction): 以上整个过程我们可以称其为引入;是实现将现有类中添加新方法和属性。
织入(Wearving): 织入是当切面应用到目标对象后创建新的代理对象的过程。
那么我们用一张图来概括上面的内容吧:
解释几个点:
- Spring切面可以应用五种类型的通知:
- Before —>在方法被调用之前调用通知
- After —>在方法完成之后调用通知,无论方法执行是否成功。
- After-returning —>在方法成功执行之后调用通知。
- After-throwing —>在方法是抛出异常后调用通知。
- Around —>通知包裹了呗通知的方法,在呗通知的方法调用之前后调用之后执行自定义的行为。
- 切面在指定的连接点被织入到目标对象中。在目标对象的声明周期里有多个点可以进行织入:
- 编译期 —>切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期 —>切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5就支持以这种方式的织入切面。
- 运行期 —>切面在应用运行的某个时刻被织入。一般情况下,在织入切面时AOP容器会为目标对象动态的创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring对AOP的支持
AOP领域主要以下列三种框架为主:
- AspectJ
- JBoss AOP
- Spring AOP
不是所有的AOP框架都是一样的,它们在连接点的模型上可能有强弱之分,它们织入切面的方式和时机也会有所不同。但是无论哪种,创建切点来定义切面织入的连接点是AOP框架的基本功能。
Spring提供了4种各具特色的AOP支持:
基于代理的经典AOP
@AspectJ注解驱动的切面
纯POJO切面
注入式AspectJ切面(适合Spring各版本)
注意: 前三种都是Spring基于代理的AOP的变体,Spring对AOP的支持局限于方法拦截
关于Spring AOP框架的一些关键点:
- Spring通知是Java编写的(AspectJ则是用以Java语言扩展的方式实现的)
- Spring在运行期通知对象
- Spring只支持方法连接点
解释:
-
我们必须了解一下Spring是如何实现在运行期通知对象的:
- 因为Spring基于动态代理,所以Spring只支持方法连接点。其他框架如AspectJ和Jboss,除了方法切点,它们还提供了字段和构造器接入点。若需要方法拦截之外的连接点可利用AspectJ来协助。
使用切点选择连接点
在Spring AOP中,需要使用AspectJ的切点表达式语言来定义切点。但是:Spring仅支持AspectJ切点指示器的一个子集(Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的)。
Spring AOP所支持的切点表达式:
arg() —>限制连接点匹配参数为指定类型的执行方法
@args() —>限制连接点匹配参数由指定注解标注的执行方法
execution() —>用于匹配是连接点的执行方法
this() —>限制连接点匹配AOP代理的Bean引用为指定类型的类
target() —>限制连接点匹配目标对象为指定类型的类
@target() —>限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
within() —>限制连接点匹配指定的类型
@within() —>限制连接点匹配指定注解所标注的类型(使用SpringAOP,方法定义在由指定的注解所标注的类里)
@annotation —>限制匹配带有指定注解连接点
编写切点
以上可以看到只有execution()
是唯一的执行匹配,其他都是限制匹配的,那么我们以一张图来分析一下如何编写切点。
如上图,整个execution
方法表达式以*
开头,标识我们不关系方法的返回值的类型。然后我们指定了全限定类名和方法名。对于参数列表,使用(..)
表示可以匹配任意的play()
方法。另外:
- 使用
within()
指示器限制匹配,比如within(* cn.tycoding.demo1.*)
- 可以使用一些表达式来连接多个限定。比如
&& ||
- 使用Spring的
bean()
指示器,允许在切点表达式中使用Bean的ID表示Bean。bean()
使用BeanID或Bean名称作为参数来限制切点只匹配特定的Bean。例如:execution(* xxx.price()) and bean(beanID)
在XML中声明切面
定义AOP通知器
定义AOP后置通知(不管被通知的方法是否执行成功)
定义AOP after-retturning通知
定义after-throwing通知
定义AOP环绕通知
定义切面
启用@AspectJ注解驱动的切面
定义AOP的前置通知
顶层的AOP配置元素。大多数的
元素必须包含在
元素内
为被通知的对象引入额外的接口,并透明的实现
定义切点
综上我们已经解释了Spring AOP的一下概念性问题,那么下面我们写一个小案例来体会一下Spring AOP
在写案例之前必须要提示一下,之前我们已经给出了Spring的全部jar包,其中也包含针对AOP功能的jar:
要注意这里需要Aspectjweaver
的jar文件
案例
- 定义一个
Advice.java
通知类
package demo3;
public class Advice {
public void changeBefore(){
System.out.println("price change before...");
}
public void changeAfter(){
System.out.println("price change after...");
}
public void errorAfter(){
System.out.println("price change error...");
}
}
- 定义一个
Fruits.java
接口
package demo3;
public interface Fruits {
void price();
}
- 定义一个
Fruits
的实现类Apple.java
package demo3;
public class Apple implements Fruits {
public void price(){
System.out.println("apple wang change price...");
}
}
- 编写配置文件
spring.xml
解释:
- 大多数的AOP配置元素必须在
元素的上下文内使用。
元素内,可以声明多个通知器、切面、或者切点。
- 使用
元素声明一个简单的切面。
Test测试类
@Test
public void run(){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
//Apple apple = (Apple) ac.getBean("apple");
//apple.price();
Fruits fruits = (Fruits) ac.getBean("apple");
fruits.price();
}
打印结果:
拓展:
我们使用Test测试类
中被注释的部分去获取Bean会怎样呢?
@Test
public void run(){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
Apple apple = (Apple) ac.getBean("apple");
apple.price();
//Fruits fruits = (Fruits) ac.getBean("apple");
//fruits.price();
}
就会出现如上报错,为什么呢?
原因
对于Spring AOP采用两种代理方法,一种是JDK的动态代理,一种是CGLIB。当代理对象实现了至少一个接口时,默认使用JDK动态创建代理对象。当代理对象没有实现任何接口时就是使用CGLIB
综上: JDK的动态代理是基于接口的; CJLIB是基于类的
首先我们要明白Spring AOP
是通过什么方式创建代理对象的(可以查看这篇博文:代理对象生成)。上面的案例中,Apple
实现了接口Fruits
,这一点就决定了Spring会采用JDK的动态代理方式创建代理对象。而我们使用getBean
即获取到一个返回的代理对象,这个代理对象其实是和Apple
一样实现了Fruits
接口。那么getBean()
返回的代理对象让Apple
这个实现类的引用来用,只能由他们的共同接口来引用。
改进
观察spring.xml
我们发现,下面的通知元素中pointcut
属性值都是一样的,这是因为所有的通知都是应用到同一个切点上,那么我们就可以通过
改进上述代码:
spring.xml
声明环绕通知
使用环绕通知可以完成之前前置通知和后置通知所实现相同的功能。首先环绕通知需要使用ProceedingJoinPoint
作为方法的入参。这个对象可以让我们在通知里调用被通知方法proceed()
- 创建环绕通知类:
AroundAdvice.java
package demo3;
import org.aspectj.lang.ProceedingJoinPoint;
public class AroundAdvice {
public void watchPrice(ProceedingJoinPoint joinPoint) {
try{
//前置通知
System.out.println("around: price change before...");
//执行被通知的方法
joinPoint.proceed();
//后置通知
System.out.println("around: price change after...");
} catch(Throwable t){
System.out.println("around: change error...");
}
}
}
- 在
spring.xml
中需要使用
即可
通知方法可以完成任何他需要做的事情,如果我们想讲控制权转给被通知的方法时,就可以调用proceed()
实现。
为通知传递参数
Fruits.java
package demo3;
public interface Fruits {
void changePrice(int price);
int getPrice();
}
在接口定义声明一个方法changePrice()
,并在其参数列表中写入一个参数。为了方便观察通知前后参数的变化,在这里又声明一个方法getPrice()
。
Apple.java
package demo3;
public class Apple implements Fruits {
private int price;
{
System.out.println("at first,apple price is: " + price);
}
public void changePrice(int price) {
this.price = price;
}
public int getPrice() {
return price;
}
}
为了观察参数price
的初始值,我们写了一个代码块打印price
的初始值。因为Spring AOP是采用动态代理方式创建代理对象的,所以都要定义接口和实现类,这里Apple
实现了Fruits
.
- 创建实现传递参数功能的通知类:
ParamAdvice.java
package demo3;
public class ParamAdvice {
private int price;
public void changePrice(int price) {
System.out.println("boss decision change price...");
this.price = price;
}
}
这是一个可以传递参数的通知类,可以看到,这与普通的类并没有什么区别,只是多了一个参数而已。
spring.xml
织入一个可以实现传递参数的通知,我们首先要在
下的
中引入通知类的id;然后可以配置一个通用的切入点
,在使用execution()
编写切点有所不同,首先在切入的方法changePrice()
中指明切入的参数数据类型,再通过and args()
来指明切入点的参数名称。
Test测试类
@Test
public void run() {
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
Fruits fruits = (Fruits) ac.getBean("apple");
fruits.changePrice(12);
System.out.println("now,this apple price is :" + fruits.getPrice());
}
这里还是要注意:因为Spring AOP采用JDK的动态代理和CGLIB的方式创建代理对象,所以对于这里有接口的实现类就必须用实现类和代理对象够共同具有的接口来接收getBean()
获取的代理对象,然后我们就可以通过Fruits
接口中的changePrice()
设置参数的值。
通过切面引入新功能
之前我们一直探讨的是Spring AOP如何编写一个简单的通知类,即使上面我们实现了为通知传递参数,也仅仅是在目标类原有方法上进行的操作。其实Spring AOP可以借助
元素来为目标对象添加信息的功能。我们用一张图来看一下实现流程:
实现代码
- 创建一个新的接口
Orange.java
package demo3;
public interface Orange {
void wantChangePrice();
}
- 创建
Orange
接口的实现类
package demo3;
public class OrangeAdvice implements Orange {
public void wantChangePrice() {
System.out.println("It's new method.Orange want change price...");
}
}
spring.xml
声明了此切面所通知的Bean在它的对象层次结构中拥有新的父类型;types-matching
指明创建的新类型匹配此接口;delegate-ref
或default-impl
都是指明匹配接口的实现类,区别是前者指明一个引用Bean,后置是直接写Bean的全限定名。
Test测试类
@Test
public void run() {
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
Orange orange = (Orange) ac.getBean("orangeAdvice");
orange.wantChangePrice();
}
注解切面
Spring AOP提供了一个@AspectJ
注解,实现了使其不需要额外的类或Bean声明就能将它转换成一个切面。不同的通知类型对应不同的注解,如:@Before()
@After()
@AfterReturning()
@AfterThrowing()
。下面我们通过一个案例来观察:
- 首先新创建一个
Melon
类
package demo3;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Melon {
//使用注解声明Bean对象
@Bean
public Melon melon(){
return new Melon();
}
public void changePrice(){
System.out.println("melon also need change price...");
}
}
如果使用注解注入一个Bean对象,要使用@Configuration
标记Java类,并通过@Bean
标记一个return 对象
的方法,告诉Spring这个方法将返回一个对象,且该对象应该被注册为Spring上下文中的一个Bean。
- 创建一个基于注解的通知类
AnnoAdvice
package demo3;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Aspect
@Configuration
public class AnnoAdvice {
@Pointcut("execution(* demo3.Melon.changePrice(..))")
public void price(){
}
//后置通知
@After("price()")
public void changeAfter(){
System.out.println("after melon price change...");
}
//使用注解注入Bean对象
@Bean
public AnnoAdvice annoAdvice (){
return new AnnoAdvice();
}
}
使用@Aspect
标记的类会被Spring转换为一个切面,那么我们可以用@Pointcut
定义一个切点,但是,注意被@Pointcut
标记的方法内容并无意义,但是此方法名称则是切点名称,该方法文本只是一个标识,供@pointcut
注解依附。
spring.xml
通过
可以实现注解扫描,这样我们在类中写的注解就会被扫描到。
是在Spring上下文中声明一个自动代理的Bean,这样Spring就会将被@Aspect
注解标记的Bean定义为一个切面,而Spring自动代理的Bean就会与用@Pointcut
注解定义的切点相匹配。
Test测试类
@Test
public void run2(){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
Melon melon = (Melon) ac.getBean("melon");
melon.changePrice();
}
注意
-
仅仅使用@AspectJ
注解作为指引来创建基于代理的切面,但本质上它仍然是一个Spring风格的切面。这就意味着我们仍然限于代理方法的调用,想使用AspectJ的全部功能就必须在运行时使用AspectJ并不依赖Spring来创建基于代理的切面。-
元素和@Aspect
注解都是把一个POJO转变成一个切面的有效方式。但是
相比@Aspect
优势就是不需要实现切面的代码,而@Aspect
必须标注类和方法,需要由源码。
-
注解环绕通知
使用Spring的@Around
注解就可以实现环绕通知,Demo如下:
我们改进上面的AnnoAdvice.java
package demo3;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
@Aspect
@Configuration
public class AnnoAdvice {
@Pointcut("execution(* demo3.Melon.changePrice(..))")
public void price(){
}
//创建环绕通知
@Around("price()")
public void aroundAdvice(ProceedingJoinPoint joinPoint){
try {
System.out.println("before, it's around advice...");
joinPoint.proceed();
System.out.println("after, it's around advice...");
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
同样,@Around
标注的方法也需要一个ProceedingJoinPoint
对象作为入参,以便可以通过proceed()
调用被代理的方法。
传递参数给所标注的通知
通过注解传递参数与XML传递参数相似,只需要改变切点的表达式就能实现。
@Pointcut("execution(* demo3.Xxx(int)) && args(xxx)")
@Before("Xxx(xx)")
标注引入
前面我们使用了XML实现在原有类中引入新的接口,其实使用注解也可以实现。XML的
对应 @AspectJ的@DeclareParents
。观察一下代码:
Melon
“瓜”类
package demo3;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Melon implements WaterMelon{
//使用注解声明Bean对象
@Bean
public Melon melon(){
return new Melon();
}
public void changePrice(){
System.out.println("melon also need change price...");
}
}
创建WaterMelon
“西瓜”接口
package demo3;
public interface WaterMelon {
void changePrice();
}
通知类AnnoAdvice
package demo3;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Aspect
@Configuration
public class AnnoAdvice {
//使用注解注入Bean对象
@Bean
public AnnoAdvice annoAdvice (){
return new AnnoAdvice();
}
@DeclareParents(value = "demo3.Fruits", defaultImpl = Melon.class)
private static WaterMelon waterMelon;
}
如上所示,AnnoAdvice
其实就是一个切面,通过@DeclareParents
注解,为Fruits
引入了一个新的接口WaterMelon
,该接口的实现类是Melon
,
对于@DeclareParents
注解由3个部分组成:
- value属性 等同于
的types-matching
属性。它表示应该被引入指定接口的Bean类型。(目标对象) - defaultImpl属性 等同于
的default-impl
属性。它标识该类提供了所引入接口的实现。(引入的新接口的实现) - @DeclareParents 注解所标注的
static
指定了被引入的接口。(引入的新的接口)
交流
如果大家有兴趣,欢迎大家加入我的Java交流群:671017003 ,一起交流学习Java技术。博主目前一直在自学JAVA中,技术有限,如果可以,会尽力给大家提供一些帮助,或是一些学习方法,当然群里的大佬都会积极给新手答疑的。所以,别犹豫,快来加入我们吧!
联系
If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.
- Blog@TyCoding's blog
- GitHub@TyCoding
- ZhiHu@TyCoding