目录
1. AOP中的基本概念
2. Spring中建立AOP应用的步骤
2.1. 添加依赖
2.2. 启用Aspect J注解支持
2.3. 定义切面
2.4. 定义切点
2.5. 定义通知方法
2.6. 编写业务类
2.7. 运行应用并测试
3. @within注解
4. AOP的优点
面向方面编程(Aspect-Oriented Programming,简称AOP)是一种编程范式,旨在通过分离横切关注点(cross-cutting concerns)来提高代码的模块化和可维护性。横切关注点是指那些影响多个模块但又不属于核心业务逻辑的功能,比如日志记录、安全检查、事务管理等。
AOP中主要概念有:连接点、切点、通知、目标对象、引介、织入、代理和切面。
(1)连接点(JointPoint)
一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就称为连接点。例如,类开始初始化前、类初始化后,类某个方法调用前、调用后,方法抛出异常后。Spring AOP仅支持方法的连接点,即仅能在方法调用前、方法调用后和方法抛出异常时这些连接点织入通知(Advice)。连接点有两个信息确定:用方法表示的执行点和用相对点表示的方位。
(2)切点(Pointcut)
每个类都拥有多个连接点,如一个拥有两个方法的类,这两个方法都是连接点,即连接点是程序类中客观存在的事物。但是在这为数众多的连接点中,如何定位到某个感兴趣的连接点上呢?AOP通过“切点”定位特定连接点。可以通过数据库查询的概念来理解切点和连接点的关系,连接点相当于数据库中的记录,而切点相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。
(3)通知(Advice)
通知是织入到目标类连接点上的一段程序代码。在Spring AOP中,通知除了用于描述一段程序代码外,还拥有另一个和连接点相关的信息,这便是执行点的方位。结合执行点方位信息和切点信息,我们就可以找到特定的连接点。
(4)目标对象(Target)
目标对象就是通知逻辑织入的目标类。如果没有AOP,目标业务类需要自己实现所有的逻辑。在AOP的帮助下,业务类只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑可以使用AOP动态织入到特定的连接点上。
(5)引介(Introduction)
引介是一种特殊的通知,它为类添加一些属性和方法。即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现。
(6)织入(Weaving)
织入是将通知添加到目标类具体连接点上的过程。AOP就像一台织布机,将目标类、通知或者引介通过AOP这台织布机天衣无缝地编织到一起。根据不同的实现技术,AOP有三种织入方式:
Spring AOP采用动态代理织入,AspectJ采用编译期织入和类装载期织入。
(7)代理(Proxy)
一个类被AOP织入通知后,就产生了一个结果类,它是融合了原类和通知逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以我们可以采用调用原类相同的方式调用代理类。
(8)切面(Aspect)
切面由切点和通知(引介)组成,它既包括了横切逻辑的定义,也包括了连接点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
首先确保项目中包含Spring AOP和AspectJ的依赖。对于Maven项目,可以在pom.xm中添加以下依赖:
<dependency> <groupId>org.springframeworkgroupId> <artifactId>spring-aopartifactId> <version>5.2.25.RELEASEversion> dependency> |
在Spring配置类上添加@EnableAspectJAutoProxy注解,开启Spring对AspectJ自动代理功能的支持。示例代码如下:
package com.my.examples.example4;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration //定义Java配置类
@EnableAspectJAutoProxy //开启Spring对AspectJ自动代理的支持
@ComponentScan("com.my.examples.example4") //自动扫描包com.my.examples.example4下所有的Spring组件
public class MyAOPConfig {
}
创建一个类,使用@Aspect注解将其标识为一个切面类,使用@Component注解将切面类标识为Spring组件。示例代码如下:
package com.my.examples.example4;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect //声明一个切面
@Component //让此切面成为一个Spring组件
public class MyAspect {
}
在切面类中使用@Pointcut注解定义切点,指定需要拦截的方法或类。示例代码如下:
package com.my.examples.example4;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //声明一个切面
@Component //让此切面成为一个Spring组件
public class MyAspect {
/**
* 定义切点,该切点表达式的含义是匹配包com.my.examples.example4中任意类的任意方法
*/
@Pointcut("execution(* com.my.examples.example4.*.*(..))")
private void myPointcut(){
}
}
execution是使用最为广泛的切点表达式,其表示满足某一匹配模式的所有目标类方法连接点,定义如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) |
其中,
切点表达式中的通配符主要有两种:
*通配符:该通配符主要用于匹配单个单词,或者是以某个词为前缀或后缀的单词。例如,在上述切点表达式中,*表示方法返回任意数据类型。
..通配符:该通配符表示0个或多个项,主要用于declaring-type-pattern和param-pattern中,如果用于declaring-type-pattern中,则表示匹配当前包及其子包,如果用于param-pattern中,则表示匹配0个或多个参数。
在切面类中定义通知方法,使用相应的注解,如@Before、@After、@AfterReturning、@AfterThrowing或@Around来指定通知的类型和执行时机。通知方法中编写需要执行的增强逻辑,如日志记录、性能监测等。示例代码如下:
package com.my.examples.example4;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect //声明一个切面
@Component //让此切面成为一个Spring组件
public class MyAspect {
/**
* 定义切点,该切点表达式的含义是匹配包com.my.examples.example4
* 中任意类的任意方法
*/
@Pointcut("execution(* com.my.examples.example4.*.*(..))")
private void myPointcut(){
}
//前置通知
@Before("myPointcut()") //使用切点myPointcut()
public void before(JoinPoint jp){
System.out.println("前置通知: " + jp.getSignature().getName());
}
//后置返回通知
@AfterReturning("myPointcut()") //使用切点myPointcut()
public void afterReturning(JoinPoint jp){
System.out.println("后置返回通知: " + jp.getSignature().getName());
}
//环绕通知
@Around(value="myPointcut()") //使用切点myPointcut()
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object obj=null;
System.out.println("环绕方法开始...");
System.out.println("目标对象:"+pjp.getTarget()+",被织入的方法:"+pjp.getSignature().getName());
obj=pjp.proceed();
return obj;
}
//后置通知
@After("myPointcut()")//使用切点myPointcut()
public void after(JoinPoint jp) {
System.out.println("后置通知...");
}
}
Spring AOP定义5类通知(注解方式):@Before、@AfterReturning、@AfterThrowing、@After、@Around,如下表所示:
通知类型 | 功能描述 |
@Before |
该通知在其切点表达式中定义的方法之前执行。Before通知处理前,目标方法还未执行,所以使用Before通知无法返回目标方法的返回值。 |
@AfterReturning |
该通知在切点表达式中定义的方法后执行。使用该通知时可以指定属性:point/value:用来指定切入点对应的切入点表达式,可以是已定义的切入点,也可以直接定义切入点表达式。returning:指定一个返回值形参名,通过该形参可以访问目标方法。 |
@After |
与@AfterReturning通知类似,但也有区别,@AfterReturning通知只有在目标方法成功执行完毕后才会被织入,而@After通知不管目标方法是正常结束还是异常中止,均会被织入。 |
@AfterThrowing |
主要用于处理程序中未处理的异常。使用AfterThrowing通知可以指定如下属性: point/value:用来指定切入点对应的切入点表达式。 throwing:指定一个返回值形参名,通过该形参访问目标方法中抛出但未处理的异常对象 |
@Around |
该通知等于@Before通知和@AfterReturning通知的总和,但与它们不同的是,@Around通知还可以决定目标方法什么时候执行,如何执行,甚至可以阻止目标方法的执行。@Around可以改变目标方法的参数值,也可以改变目标方法的返回值。 在定义@Around通知的切面逻辑时,必须给方法至少加入ProceedingJoinPoint类型的参数,在方法内调用ProceedingJoinPoint的proceed()方法才会执行目标方法,调用proceed()方法时还可以传入一个Object[]对象,该数组中的数据将作为目标方法的实参。 |
在定义多个通知时,通知的执行次序与优先级由低到高为:Before、Around 、After 、AfterReturning 。
业务逻辑类和方法不需要任何AOP相关的代码,只需确保它们符合切点表达式。示例代码如下:
package com.my.examples.example4;
import org.springframework.stereotype.Service;
@Service //定义Spring构件
public class MyService {
public void performTask() {
System.out.println("Performing task");
}
}
启动Spring应用,并调用目标类的方法进行测试。AOP切面会根据定义的切点和通知自动拦截目标方法的执行,并执行相应的通知逻辑。示例代码如下:
package com.my.examples.example4;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
public static void main(String[] args) {
//根据Java配置类创建Spring容器
AnnotationConfigApplicationContext context = new
AnnotationConfigApplicationContext(MyAOPConfig.class);
MyService myService=context.getBean(MyService.class);
myService.performTask();
}
}
运行类Test,在控制台输出如下信息:
环绕方法开始... 目标对象:com.my.examples.example4.MyService@61710c6,被织入的方法:performTask 前置通知: performTask Performing task 后置返回通知: performTask 后置通知... |
在Spring AOP中,@within用于匹配目标类具有指定注解的所有方法,如果一个类上有指定的注解,那么该类的所有方法都会被匹配。示例代码如下:
(1)自定义注解
package com.my.examples.example5;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
}
(2)带自定义注解的类
package com.my.examples.example5;
import org.springframework.stereotype.Service;
@MyAnnotation //自定义注解
@Service //定义Spring组件
public class MyService {
public void performTask() {
System.out.println("Performing a task.");
}
}
(3)切面类
package com.my.examples.example5;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
@Before("@within(com.my.examples.example5.MyAnnotation)")
public void before1(JoinPoint joinPoint) {
System.out.println("前置通知:"+joinPoint.getSignature().getName());
}
}
(4)启用Aspect J注解支持
package com.my.examples.example5;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration //定义Java配置类
@EnableAspectJAutoProxy //开启Spring对AspectJ自动代理的支持
@ComponentScan("com.my.examples.example5") //自动扫描包com.my.examples.example5下所有的Spring组件
public class MyAOPConfig {
}
(5)运行应用测试
package com.my.examples.example5;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new
AnnotationConfigApplicationContext(MyAOPConfig.class);
MyService service = context.getBean(MyService.class);
service.performTask();
}
}
运行类Test,在控制台输入如下信息:
前置通知:performTask Performing a task. |
(1)代码集中。解决了由于OOP跨模块造成的代码纠缠和代码分散问题。
(2)模块化横切关注点。核心业务级关注点与横切关注点分离开,降低横切模块与核心模块的耦合度,实现了软件工程中的高内聚、低耦合的要求。
(3)系统容易扩展。AOP的基本业务模块不知道横切关注点的存在,很容易通过建立新的切面加入新的功能。另外,当系统中加入新的模块时,已有的横切面自动横切进来,使系统易于扩展。
(4)提高代码重用性。AOP把每个Aspect实现为独立的模块,模块之间松散耦合,意味着更高的代码重用性。