AOP -面向切面编程
aop是oop(面向对象编程)的补充和完善。oop是一种纵向开发,然而当面对需要一些横向的功能如日志,就会导致大量重复的代码。
aop利用横切把一些重复的非核心的代码插入到核心代码周围,不需要重复写很多遍。
应用场景:
-
日志记录,在方法的执行前后插入日志功能
-
事务处理,在方法开始前开启事务,方法结束后提交事务或回滚
-
安全控制,在系统中包含某些需要安全控制的操作,进行权限判断等
-
性能监控,在方法执行前记录事件戳,方法执行完后计算方法执行的时间
-
异常处理,处理方法过程中的异常,可以记录日志、发送邮件等
-
缓存控制,在方法执行前查询缓存中是否有数据
-
动态代理,AOP的实现方式之一就是动态代理
1.AOP中的名词
-
横切关注点:业务处理的主要流程是核心关注点,其他的一些非核心的业务如权限认证、日志、事务等就是横切关注点,他们发生在核心关注点的多处
-
通知(通知):提取出来的重复的非核心代码
-
前置通知:在被代理的目标方法前执行
-
返回通知:在被代理的目标方法成功结束后执行
-
异常通知:在被代理的目标方法异常结束后执行
-
后置通知:在被代理的目标方法最终结束后执行
-
环绕通知:使用 try...catch...finally 结构环绕整个被代理的目标方法,包括上面四种 通知对应的所有位置
-
-
连接点: 能够被插入通知的 方法
-
切入点:被选中的连接点,也就是真正被插入通知的方法
-
切面:切入点和通知的结合,是一个类
-
目标: 被代理的目标对象,也就是向哪个对象中插入代码
-
代理:向目标对象应用通知后创建的对象
-
织入:把通知应用到目标对象上这个动作
2.AOP的实现
2.1 aop的底层技术
在底层是通过代理技术来实现的,主要分为两种情况:jdk动态代理和cglib
-
jdk动态代理是原生的实现方式,需要被代理的目标类必须实现接口。然后通过接口创建一个 代理类,所以代理对象和目标对象实现了同一个接口。
-
cglib:代理类继承了目标类,不需要目标类有接口
在实现层的上一层还有 AspectJ 注解层,AspectJ是早期的AOP实现框架,SpringAOP借助了这个框架来实现
所以在导入依赖的时候需要导入 spring的aop依赖和 aspectj的依赖,以及这两个框架整合的依赖。
2.2 初步使用aop
使用aop的前提是有横向切入的需求,这里我们通过 给计算类添加日志的功能来使用aop。
-
导入依赖
由上面可以得知,需要三个依赖:aop、aspectj、spring-aspectj
aop依赖可以通过spring-context 进行传递, aspectj 可以通过spring-aspectj依赖进行传递
所以导入 spring-context 和 spring-aspectj即可
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.6</version> </dependency>
-
正常写计算类的核心业务代码
-
准备接口(有接口的话,底层的实现技术就是 jdk 动态代理)
package com.ztone.service; public interface Calculate { int add(int i,int j); int sub(int i,int j); int mul(int i,int j); int div(int i,int j); }
-
实现计算接口
package com.ztone.impl; import com.ztone.service.Calculate; import org.springframework.stereotype.Component; @Component public class CalculateImpl implements Calculate { @Override public int add(int i, int j) { int result = i + j; return result; } @Override public int sub(int i, int j) { int result = i - j; return result; } @Override public int mul(int i, int j) { int result = i * j; return result; } @Override public int div(int i, int j) { int result = i / j; return result; } }
不要忘了把这个类放入ioc容器,因为aop功能只针对aop容器内的对象
-
-
编写通知类,并且定义通知方法
现在我们想在 CalculateImpl 类中的四个计算方法的前后都加上日志,那么就需要一个类来写这个重复的代码,这个类就是通知类或者叫增强类,这个类中的方法就是增强方法,里面存放增强代码
具体的要把这些方法插入到哪个位置,用注解来指定
-
前置通知 @Before
-
后置通知 @AfterReturning
-
异常通知 @AfterThrowing
-
最后通知 @After
-
环绕通知 @Around
知道了哪个增强方法放在核心代码的前或后,还需要配置切点表达式,就是要把这些增强方法插入到哪个类的哪个方法,切点表达式是就是这些注解的 参数 例如 @Before("execution(* com.ztone.impl.* .* (..))")
execution(* com.ztone.impl.* .* (..))
这个表达式 execution是固定写法,括号中 第一个星号表示不考虑方法的返回值和修饰符,com.ztone.impl 表示哪个包,第二个星号表示这个包下的哪个类,第三个星号表示这个类下的哪个方法,后面括号中两个点表示不考虑这个方法的返回值
package com.ztone.advice; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Component @Aspect public class LogAdvice { @Before("execution(* com.ztone.impl.*.*(..))") public void start(){ System.out.println("方法开始了"); } @AfterReturning("execution(* com.ztone.impl.*.*(..))") public void after(){ System.out.println("方法结束了"); } @AfterThrowing("execution(* com.ztone.impl.*.*(..))") public void error(){ System.out.println("方法出错了"); } }
增强类也要放入ioc容器 用 @Component
还要使用 @Aspect 注解 表示该类是一个切面
-
-
在配置类中开启aspect注解的支持
使用 @EnableAspectJAutoProxy 注解
package com.ztone.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan("com.ztone") @EnableAspectJAutoProxy public class JavaConfig { }
-
测试
@SpringJUnitConfig(value = JavaConfig.class) public class CalculateTest { @Autowired private Calculate calculate; @Test public void test(){ int add = calculate.add(1, 1); System.out.println(add); } }
这里用的是spring 的junit测试,不用自己创建aop容器
还需要注意的一点是:在声明 CalculateImpl 类时,用的是 接口接值,原因是有了接口底层实现就是用的jdk动态代理,然后根据这个接口创建一个代理类,如果用 目标类去接值的话,对象的类型就不同了会报错
ioc容器中存储的 也是 创建的代理类,而不是 目标类
2.3 在增强方法中获取目标方法信息
-
获取目标方法的信息(方法名、参数、访问修饰符、所属类的信息),可以在所有增强方法中获取
需要在要获取信息的增强方法中添加一个参数 JoinPoint
@Component @Aspect public class MyAdvice { @Before("execution(* com.ztone.impl.*.*(..))") public void before(JoinPoint joinPoint){ //获取类的信息 String simpleName = joinPoint.getTarget().getClass().getSimpleName(); //获取目标方法名和修饰符 String name = joinPoint.getSignature().getName(); joinPoint.getSignature().getModifiers(); //获取参数列表 Object[] args = joinPoint.getArgs(); } }
-
获取目标方法的返回值,只能在后置增强中获取 @AfterReturning
-
在后置增强方法中添加一个Object 类型的参数
-
在@AfterReturning 注解中添加 returning 属性,值就是后置增强方法的形参
@AfterReturning(value = "execution(* com.ztone.impl.*.*(..))",returning = "returning") public void afterReturning(Object returning){ System.out.println(returning); }
返回值就是 后置增强方法的形参
-
-
获取目标方法的异常,只能在异常增强中获取 @AfterThrowing
-
在异常增强方法中添加一个Throwable类型的参数
-
在 @AfterThrowing 注解中添加 throwing 属性,值是 异常增强方法的形参
@AfterThrowing(value = "execution(* com.ztone.impl.*.*(..))",throwing = "throwable") public void afterThrowing(Throwable throwable){ }
-
2.4 切面表达式
固定语法:execution(1 2 3.4.5(6))
-
访问修饰符
四个访问修饰符 public、private、default、protect
-
方法返回值
String、int 、void 等
访问修饰符和返回值 绑定在一起,如果两者都不考虑可以用 * 代替,但是不能只考虑一个而另一个不考虑
-
包的位置
com.ztone.service.impl
单层模糊:com.ztone.service.* 表示 service包下的所有包
多层模糊:com..impl 表示com 和 impl 包之间可以有任意层
但是.. 不能开头,开头只能是包名或 *
-
类的名称
CalculateImpl
模糊:用 * 代替表示所有类
部分模糊: *Impl 表示以Impl结尾的类
-
方法名称
和类相同
-
形参列表
没有参数:()
有具体参数:(String,int)
模糊参数:(..) 所有参数都行
部分模糊:(String ..) 第一个参数是String
切点表达式的提取和复用:
如果在多个增强方法中的切点表达式相同,那么就可以把切点表达式提取出来,后期修改直接修改一个即可。
使用到的注解是 @Pointcut
在一个方法上使用这个注解,注解的值就是提取的切点表达式,然后再增强方法的注解上调用这个方法即可。
package com.ztone.pointcut;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
public class MyPointcut {
@Pointcut("execution(* com.ztone.impl.*.*(..))")
public void myPc(){}
}
这里是在一个类中提取出来,以保证在其他类中也能调用到该方法
@Component
@Aspect
public class LogAdvice {
@Before("com.ztone.pointcut.MyPointcut.myPc()")
public void start(){
System.out.println("方法开始了");
}
@AfterReturning("com.ztone.pointcut.MyPointcut.myPc()")
public void after(){
System.out.println("方法结束了");
}
@AfterThrowing("com.ztone.pointcut.MyPointcut.myPc()")
public void error(){
System.out.println("方法出错了");
}
}
使用的时候就是用类的全限定符.方法名
2.5 环绕通知
环绕通知就是将 前置、后置、异常等通知结合起来。
比如给一个目标方法添加事务,可以在前置通知中开启事务,在后置通知中结束事务,在异常通知中进行事务回滚
需要写三个通知方法
如果使用环绕通知,用一个方法就够了,使用的注解是 @Around
该方法接收一个参数 ProceeedingJoinPoint ,这个参数可以获取到目标方法的信息并且可以执行目标方法,使用proceed()
环绕通知方法必须返回一个Object 类型的返回值,这个值就是执行目标方法返回的值
package com.ztone.advice;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class TxAdvice {
@Around("com.ztone.pointcut.MyPointcut.myPc()")
public Object txAd(ProceedingJoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
Object result = null;
try {
System.out.println("事务开启");
result = joinPoint.proceed(args);
System.out.println("事务结束");
} catch (Throwable e) {
System.out.println("事务回滚");
throw new RuntimeException(e);
}
return result;
}
}
joinPoint.proceed(args); 就是执行目标方法
2.6 通知的优先级
如果有多个通知,想要某个通知先执行可以用 @Order 注解来设置通知的优先级,注意@Order的值越小优先级越高
优先级高的通知,前置通知会先执行,后置通知最后执行,相当于把优先级低的通知包裹起来
3.xml方式实现aop
-
首先准备一个增强类
@Component public class LogAdvice { public void start(){ System.out.println("方法开始了"); } public void after(){ System.out.println("方法结束了"); } public void error(){ System.out.println("方法出错了"); } }
一定要使用@Component 加入到aop容器中
-
在xml标签中配置
-
在最外层用 <aop:config > 标签包裹所有的aop配置
-
声明切点标签,使用 <aop:pointcut > 相当于 @PointCut 注解
-
id:切点的标识符
-
expression:切点表达式
-
-
配置切面 使用 <aop:aspect > 相当于 @Aspect 注解
-
ref :引用的增强类
-
order:切面的优先级
-
在标签里面使用
-
<aop:before > 表示前置增强
-
method:增强类中的前置增强方法
-
pointcut-ref:引用的切点,或者可以使用 pointcut直接指定切点表达式
-
-
<aop:after-running > 前两个同理,returning属性指定后置方法的返回值
-
<aop:after-throwing > 前两个同理,throwing属性指定后置方法的返回值
-
-
-
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.ztone"/>
<aop:config>
<aop:pointcut id="myPc" expression="execution(* com.ztone.service.impl.*.*(..))"/>
<aop:aspect ref="txAdvice" order="5">
<aop:before method="before" pointcut-ref="myPc"/>
<aop:after-returning method="afterReturning" pointcut-ref="myPc" returning="returning"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="myPc" throwing="t"/>
</aop:aspect>
</aop:config>
</beans>