面向切面编程
面向切面编程【AOP,Aspect Oriented Programming】:通过预编译方式和运行期间动态代理实现程序功能的统一维护的技术。AOP 是 Spring 框架中的一个重要内容,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
在 Spring 中,依赖注入管理和配置应用对象,有助于应用对象之间的解耦。而面向切面编程可以实现横切关注点与它们所影响的对象之间的解耦。
横切关注点:散布在应用中多处的功能,可以被提取出来集中处理。
面向切面编程所要解决的问题是:将横切关注点与应用的业务逻辑相分离。
AOP 常见的场景:日志、声明式事务、安全和缓存。
使用面向切面编程时,在一个地方定义通用功能,然后通过声明的方式定义这个通用功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,该类被称为切面。
好处
- 每个关注点集中在一个地方,而不是分散在多处代码中;
- 模块更简洁,主要的代码只关注业务逻辑代码;
1、专业术语
通知(Advice)
切面的工作被称为通知。通知定义了切面是什么以及何时使用。Spring 含有 5 中类型的通知:
- 前置通知(Before):在目标方法被==调用之前==调用通知;
- 后置通知(After):在目标方法被==调用之后==调用通知,此时不关心方法的输出是什么;
- 返回通知(After-returning):在目标方法==成功执行后==调用通知;
- 异常通知(After-throwing):在目标方法==抛出异常后==调用通知;
- 环绕通知(Around):在目标方法==调用之前和调用之后==均调用通知;
连接点(Join point)
连接点是应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。简单理解:一个方法即为一个连接点,只是在这个连接点调用通知的时间点可以自定义。
切点(Pointcut)
切点是一定数量的连接点;切点定义所要织入通知的一个或多个连接点。Spring 基于动态代理,只支持方法连接点。切点指明目标方法,当目标方法调用执行时,会调用对应的通知。
切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的功能、在何时和何地完成功能。
引入(Introduction)
向现有的类添加新的方法或属性称为引入。
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。目标对象的生命周期里有多个点可以织入切面:
- 编译期:切面在目标类编译时被织入;需要使用特殊的编译器,比如:AspectJ 的织入编译器。
- 类加载器:切面在目标类加载到 JVM 时被织入;
- 运行期:切面在应用运行在某个时刻被织入;一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。
2、Spring的AOP实现
Spring 提供了 4 种类型的 AOP 支持:
- 基于代理的经典 Spring AOP;
- 纯 POJO 切面;
@AspectJ
注解驱动的切面;- 注入式 AspectJ 切面(适用于 Spring 各版本);
前三种都是 Spring AOP 实现的变体,Spring AOP 构建在动态代理基础之上,因此,Spring 对 AOP 的支持局限于 方法拦截。
Spring 通知是用标准的 Java 类编写的。定义通知所应用的切点通常使用注解或在 Spring XML 配置文件中编写。
Spring 在运行时通知对象。Spring 的切面由包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。直到装配需要被代理的 Bean 时,Spring 才会创建代理对象。
2.1 AspectJ 指示器
前面讲到,切点作用是用于定位,在所在位置执行时调用切面的通知。在 Spring AOP 中,要使用 AspectJ 的切点表达式来定义切点;而在 Spring AOP 所支持的 AspectJ 切点指示器有:
指示器 | 描述 |
---|---|
arg() |
限制连接点匹配参数为指定类型的执行方法; |
@args() |
限制连接点匹配参数由指定注解标注的执行方法; |
execution() |
用于匹配是连接点的执行方法; |
this() |
限制连接点匹配 AOP 代理的 Bean 引用为指定类型的类; |
target() |
限制连接点匹配目标对象为指定类型的类; |
@target() |
限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解; |
within() |
限制连接点匹配指定的类型; |
@within() |
限制连接点匹配指定注解所标注的类型; |
@annotation() |
限定匹配带有指定注解的连接点; |
@bean() |
限定连接点匹配指定 ID 的 Bean; |
上述指示器中,除了 execution
之外,均用于匹配连接点;而 execution
指示器是执行通知:当括号内的连接点对应的方法被调用时,execution
会执行对应的通知操作。
2.2 使用注解创建切面
2.2.1 创建切面类
AspectJ 5 引入了重要的特性:使用注解创建切面。要在类中使用注解创建切面,首先,必须要导入对应的 jar包( aspectjweaver.jar
),下面,我们来创建一个切面类:
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AspectClass {
}
使用注解 AspectJ
表明,这个类不仅仅是一个简单的 Java 类,还是一个切面。但是前面说过,一个完整的切面应该包含切点和通知。
2.2.2 定义通知方法
在切面类中,5 种通知类型分别对应 5 种注解,使用注解标注方法定义通知方法,这些注解分别是:
注解 | 通知 |
---|---|
@After | 目标方法返回或抛出异常后调用 |
@AfterReturning | 目标方法返回后调用 |
@AfterThrowing | 目标方法抛出异常后调用 |
@Around | 将目标方法封装起来 |
@Before | 目标方法调用之前执行 |
使用注解定义通知
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class AspectClass {
@Before()
public void before(){
System.out.println("前---置通知");
}
@After()
public void before(){
System.out.println("后---置通知");
}
@Around()
public Object around(ProceedingJoinPoint jp){
System.out.println("环绕通知---前");
Object proceed=null;
try {
Object[] args = jp.getArgs(); //获取传入目标方法的参数
proceed = jp.proceed(); //调用执行目标方法
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知---后");
return proceed;
}
}
其中,环绕通知的方法定义不同。环绕通知方法中需要传入一个参数 ProceedingJoinPoint
接口,该接口控制目标方法:
getArgs()
获取目标方法的形参;- 通过
proceed()
执行目标方法,会返回目标方法的返回值;如果不调用此方法,会阻塞目标方法的调用;也可以多次调用;
2.2.3 编写切点表达式
在注解内放入切点表达式。所谓切点表达式,是使用指示器匹配连接点。切点表达式中需要使用 execution
指示器(如下图)。
不同的指示器之间可以使用逻辑运算(and、or、not
)拼接一起使用。
下面,我们为切面添加切点,当执行器内指定的目标方法执行时会调用对应的通知方法。
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class AspectClass {
@Before("execution(* 包名.类名.目标方法())")
public void before(){
System.out.println("前---置通知");
}
@After("execution(* 包名.类名.目标方法())")
public void after(){
System.out.println("后---置通知");
}
@Around("execution(* 包名.类名.目标方法())")
public Object around(ProceedingJoinPoint jp){
System.out.println("环绕通知---前");
Object proceed=null;
try {
proceed = jp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知---后");
return proceed;
}
}
还有一种方式可以简化编写切点。在上面这个例子中,每个通知中都使用切点表达式来匹配连接点,这样做很繁琐。使用 @Poingcut
注解标注,为一个方法编写切点,然后在通知注解中引用切点方法即可。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class AspectClass {
//切点方法
@Pointcut("execution(* 包名.类名.目标方法())")
public void work() {
}
@Before("work()")//引用切点方法
public void before() {
System.out.println("前---置通知");
}
@After("work()")//引用切点方法
public void after() {
System.out.println("后---置通知");
}
@Around("work()")//引用切点方法
public Object around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕通知---前");
Object proceed = null;
Object[] args = jp.getArgs();
jp.proceed();
System.out.println("环绕通知---后");
return proceed;
}
}
2.2.4 启动自动代理
到此为止,一个切面就创建好了。但是,如果没有启用自动代理功能,这个切面只能被当做一个 Bean,AspectJ 注解也不会被解析。在 JavaConfig 配置类和 XML 配置文件启动自动代理功能。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
//(1)启动 AspectJ 自动代理
@EnableAspectJAutoProxy
public class WorkAspectConfig {
//(2)声明切面类的bean
@Bean
public AspectClass getAspectClass(){
return new AspectClass();
}
}
2.2.4 处理通知中的参数
在此之前,我们在讲到前置通知和后置通知的方法定义中都是没有参数,除了环绕通知,可以通过 ProceedJoinPoint
接口获取目标方法的参数和传递返回值。但,如果前置通知和后置通知的方法中定义了参数,那么,切面如何访问和使用目标方法中的参数呢?
@Before("execution(* 包名.类名.目标方法名(Type...)) && args(name,...)")
public void work(Type name,...){
}
不同之处在于:
Execution
指示器内的方法需要指定参数类型,这个参数类型与传入通知方法的参数类型匹配;- 使用逻辑运算符 && (或
||、!
)拼接一个args
指示器,这个指示器内指定参数名,表明传递给目标方法的 Type 类型参数也会传递给通知方法。 args
指示器内的参数名称与通知方法的名称要一致。
2.2.5 添加新功能
上面已经讲解讲解了:如何为现有方法添加额外的功能。那现在我们需要为一个对象添加新的方法,如何实现呢?具体步骤如下:
- 创建一个新的接口,接口内定义了新功能的方法;并创建新接口的实现类;
- 创建一个新的切面类,该类内定义一个步骤1声明接口的静态属性,该属性使用注解
@DeclareParents
标注; - 在配置文件中装配切面类的 Bean,以及新接口的实现类的 Bean;
这样就完成了为一个现有对象添加了新方法。注意:要使用新方法,对象需要强制转换为新接口类型。在这里需要重点了解的是:@DeclareParents
的使用
//@DeclareParents 由三部分组成
@DeclareParents(value="package.OldInterface+"
defaultImpl= newClassImplNewInterface.class)
public static NewInterface newInterface;
- value :指定需要添加新功能的类,+表示 OldInterface 类的所有子类;
- defaultImpl :指定添加了新功能的实现类;
@DeclareParents
注解标注的静态属性:指明了要引入新功能的接口;
2.3 XML 配置创建切面
在 Spring 的 XML 配置文件中, aop 命名空间提供了元素用来声明切面,如表:
AOP配置元素 | 用途 |
---|---|
|
启用 @AspectJ 注解驱动的切面 |
|
顶层AOP配置元素。大多数aop元素必须在该元素内 |
|
定义一个切面 |
|
定义AOP通知器 |
|
定义AOP后置通知 |
|
定义AOP返回通知(不管目标方法是否执行成功) |
|
定义AOP异常通知 |
|
定义AOP环绕通知 |
|
定义AOP前置通知 |
|
以透明的方式为目标对象引入额外的接口 |
|
定义一个切点 |
已经了解了 XML 配置的基本使用元素。由于前面对切面的了解已经比较深入,现在了解如何使用 XML 配置 AOP,暂不深入过多。直接上例子:
1、原有代码,需要在现有代码中添加新功能。简称:目标对象、目标方法。
package xml;
public class Work {
public String working(){
System.out.println("工作ing");
return "工作ing";
}
public void working(int time){
System.out.println("工作时长:"+time);
}
}
2、要向目标方法添加的新功能类,并以此类添加切面
package xml;
import org.aspectj.lang.ProceedingJoinPoint;
public class AspectClass {
public void before() {
System.out.println("前---置通知");
}
public void after() {
System.out.println("后---置通知");
}
public Object around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕通知---前");
Object proceed = null;
//(1)获取目标方法的参数
Object[] args = jp.getArgs();
//(2)调用目标方法,并获取返回值
proceed = jp.proceed(args);
System.out.println("环绕通知---后");
return proceed;
}
public void afterWork(int time) {
if (time > 0 && time < 8) {
System.out.println("工作时长不够8小时");
}else{
System.out.println("工作时长:"+time);
}
}
}
3、要向目标对象添加新的方法
package xml;
public interface OtherWork {
void addWorkTime();
}
package xml;
public class NightWork implements OtherWork {
@Override
public void addWorkTime() {
System.out.println("加夜班");
}
}
4、编写 XML 配置文件
6、测试类
package test;
import xml.OtherWork;
import xml.Work;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:xml/AspectConfig.xml")
public class WorkTest {
@Test
public void test(){
//解析XML配置文件
ApplicationContext app =new ClassPathXmlApplicationContext("xml/AspectConfig.xml");
//测试目标方法的新功能
Work work = (Work) app.getBean("w");
work.working(6);
//测试目标对象的新方法
OtherWork nightWork = (OtherWork) work;
nightWork.addWorkTime();
}
}
//测试结果:
// 工作时长:6
// 工作时长不够8小时
// 加夜班
到此为止,Spring AOP的基础学习完毕。