AOP 相关概念
1.横切关注点
一些具有横切多个不同软件模块的行为,通过传统的软件开发方法不能够有效地实现模块化的一类特殊关注点。横切关注点可以对某些方法进行拦截,拦截后对原方法进行增强处理。
2.切面(Aspect)
切面就是对横切关注点的抽象,这个关注点可能会横切多个对象。
3.连接点(JoinPoint)
连接点是在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。由于 Spring 只支持方法类型的连接点,所以在 Spring AOP 中一个连接点表示一个方法的执行。
4.切入点(Pointcut)
切入点是匹配连接点的拦截规则,在满足这个切入点的连接上运行通知。切入点表达式如何和连接点匹配是 AOP 的核心,Spring 默认使用 AspectJ 切入点语法。
5.通知(Advice)
在切面上拦截到某个特定的连接点之后执行的动作。
6.目标对象(Target Object)
目标对象,被一个或者多个切面所通知的对象,即业务中需要进行增强的业务对象。
7.织入(Weaving)
织入是把切面作用到目标对象,然后产生一个代理对象的过程。
8.引入(Introduction)
引入是用来在运行时给一个类声明额外的方法和属性,即不需为类实现一个接口,就能使用接口中的方法。
AOP 实现
1. 添加依赖
要实现 AOP ,我们需要添加如下的依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
## 2. 编写业务方法(也叫目标方法)
```java
public interface DemoService {
void show();
}
@Service
public class DemoServiceImpl implements DemoService {
@Override
public void show() {
System.out.println("show() 方法被执行");
}
}
2. 编写切面类
public class Log {
public void before() {
System.out.println("在目标方法执行之前会被调用,写入日志");
}
}
## 3. 编写配置文件
```xml
<context:component-scan base-package="com.jock.aop.service"/>
<bean id="log" class="com.jock.aop.log.Log"/>
<aop:config>
<aop:aspect ref="log">
<aop:pointcut id="pt" expression="execution(* com.jock.aop.service.*.*(..))"/>
<aop:before method="before" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
## 4. 编写测试类
```java
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringAopXmlTest {
@Autowired
private ApplicationContext context;
@Test
public void test() {
DemoService service = context.getBean(DemoService.class);
service.show();
}
}
切入点表示式
1. 全匹配方式
public void com.jock.service.impl.UserServiceImpl.saveUser() 匹配访问修饰符必须为 pubic,返回值类型为 void,同时在com.jock.service.impl包中的 UserServiceImpl 中的 saveUser() 方法执行时才有增强
2. 访问修饰符可以省略
void com.jock.service.impl.UserServiceImpl.saveUser() 不在乎是否为 public
3. 返回值可以使用*号, 表示任意返回值
* com.jock.service.impl.UserServiceImpl.saveUser() com.jock.service.impl.UserServiceImpl类中所有 saveUser() 方法,不管这个方法的返回值类型是什么都能匹配上
4. 包名可以使用*号, 表示任意包, 但是有几级包, 需要写几个*
* *.*.*.*.UserServiceImpl.saveUser() 任意返回值类型,任何包下的任意子包中的任意子包中的任意子名中UserServiceImpl类中的saveUser()方法
5. 使用..来表示当前包, 及其子包
* com..UserServiceImpl.saveUser() 任意返回值类型, com包下的任意子名中的 UserServiceImpl 类中的saveUser()方法
6. 类名可以使用*号, 表示任意类
* com..*.saveUser() 任意返回值类型, com包下的任意子名中的 任意类中的 saveUser() 方法
7. 方法名可以使用*号, 表示任意方法
* com..*.*() 任意返回值类型, com包下的任意子名中的 任意类中的 任意方法,注意这个方法不能有参数
8. 参数列表可以使用*, 表示参数可以是任意数据类型, 但是必须有参数
* com..*.*(*) 任意返回值类型, com包下的任意子名中的 任意类中的 任意方法,注意这个方法必须有参数
9. 参数列表可以使用..表示有无参数均可, 有参数可以是任意类型
* com..*.*(..) 任意返回值类型, com包下的任意子名中的 任意类中的 任意方法,不管这个方法有没有参数,参数的个数是多少,掌握这种写法
10. 全通配方式:
* *..*.*(..)
可以参考Spring官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-api-pointcut-ops
AOP 相关配置说明
1. <aop:config>
作用:用于声明开始 aop 的配置
2. <aop:aspect>
作用:用于配置切面。
属性:
- id: 给切面提供一个唯一标识。
- ref: 引用配置好的通知类 bean 的 id。
3. <aop:pointcut>
作用:用于配置切入点表达式
属性:
- expression: 用于定义切入点表达式。
- id: 用于给切入点表达式提供一个唯一标识。
4. <aop:before>
作用:用于配置前置通知。执行时间点: 切入点方法执行之前执行
属性:
- method: 指定通知中方法的名称。
- pointcut: 定义切入点表达式
- pointcut-ref: 指定切入点表达式的引用
5. <aop:after-returning>
作用: 用于配置后置通知。执行时间点: 切入点方法正常执行之后
属性:
- method: 指定通知中方法的名称。
- pointcut: 定义切入点表达式
- pointcut-ref: 指定切入点表达式的引用
6. <aop:after-throwing>
作用: 用于配置异常通知。执行时间点: 切入点方法执行产生异常后执行
属性:
- method: 指定通知中方法的名称。
- pointcut: 定义切入点表达式
- pointcut-ref: 指定切入点表达式的引用
7. <aop:after>
作用: 用于配置最终通知。执行时间点: 无论切入点方法执行时是否有异常, 它都会在其后面执行
属性:
- method: 指定通知中方法的名称。
- pointcut: 定义切入点表达式
- pointcut-ref: 指定切入点表达式的引用
8. <aop:around>
作用: 用于配置环绕通知
属性:
- method: 指定通知中方法的名称。
- pointcut: 定义切入点表达式
- pointcut-ref: 指定切入点表达式的引用
AOP实现方式
在 Spring 中,AOP 实现的方式动态代理,而动态代理有两种方式:
- JDK 动态代理,这种方式前提条件是被代理对象必须实现接口,而这种方式是 Spring 中默认的 AOP 实现方式
- CGLIB 动态代理,这种方式不需要被代理对象实现接口,在 Spring 中,如果希望使用 CGLIB 来实现动态代理,哪么就需要在 aop:config 标签中增加 proxy-target-class 属性,并把这个属性的值设置为 true
使用注解实现AOP功能
1. 编写切面类,并增加切面注解 @Aspect
@Component
@Aspect // 表示当前类是一个切面类
public class Logger {
@Before("execution(* com.jock.aop.service..*.*(..))")
public void before() {
System.out.println("Logger.before");
}
}
> 我们需要在增强的方法上使用 @Before 注解来指定这个方法是一个前置通知方法,并且需要在这个注解中提供切入点表达式
2. 编写配置文件
<!-- 扫描注解 -->
<context:component-scan base-package="com.jock.aop"/>
<context:annotation-config />
<!-- 开启 AOP 注解支持 -->
<aop:aspectj-autoproxy/>
> 在配置文件中,需要使用 <aop:aspectj-autoproxy/> 来开启AOP的代码支持,而 <context:annotation-config /> 用于开启注解支持
3. 编写测试类
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:spring-annotation.xml")
public class SpringAopAnnotationXmlTest {
@Autowired
private DemoService demoService;
@Test
public void test() {
demoService.show();
System.out.println("------------------");
demoService.show("zs", 18);
System.out.println("------------------");
demoService.say();
}
}
4. 在切面类中添加其他通知
@Component
@Aspect // 表示当前类是一个切面类
public class Logger {
@Before("execution(* com.jock.aop.service..*.*(..))")
public void before() {
System.out.println("Logger.before");
}
@After("execution(* com.jock.aop.service..*.*(..))")
public void after() {
System.out.println("Logger.after");
}
@AfterReturning("execution(* com.jock.aop.service..*.*(..))")
public void afterReturning() {
System.out.println("Logger.afterReturning");
}
@AfterThrowing("execution(* com.jock.aop.service..*.*(..))")
public void afterThrowing() {
System.out.println("Logger.afterThrowing");
}
@Around(value = "execution(* com.jock.aop.service..*.*(..))")
public void around(ProceedingJoinPoint joinPoint) {
try {
joinPoint.proceed();
System.out.println("Logger.around");
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
上面的方法,从运行的角度没有问题,程序能够正常的运行,并且也能实现 AOP 的功能,但是我们在每个方法上都写相同的切入点表达式,其实,我们是可以简化这部分代码的。
```java
@Component
@Aspect // 表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.jock.aop.service..*.*(..))")
private void pt() {
}
@Before("pt()")
public void before() {
System.out.println("Logger.before");
}
@After("pt()")
public void after() {
System.out.println("Logger.after");
}
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("Logger.afterReturning");
}
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("Logger.afterThrowing");
}
@Around("pt()")
public void around(ProceedingJoinPoint joinPoint) {
try {
joinPoint.proceed();
System.out.println("Logger.around");
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}