AOP代理
基本流程
- 正常业务逻辑: 导入依赖->编写核心代码(需要加入IoC)->编写IoC配置类和文件->测试环境
- 增强: 增强类,定义增强方法(存储横切关注点)->增强类的配置(插入点的位置,切点指定,切面配置等等)->开启AOP配置
添加依赖
AOP+Aspectj
<!-- AOP依赖-->
<!-- aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.11</version>
</dependency>
<!-- aspectj-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.1.11</version>
</dependency>
案例1:日志增强——普通通知
- 基本结构
日志输出----------前置通知(非核心关注点)
核心方法----------连接点(核心关注点)
日志输出----------返回增强(非核心关注点)
报错:输出日志------报错通知(非核心关注点)
准备接口
[!note]
可以不用接口cglib方式
public interface Canculator {
double add(double i, double j);
double sub(double i, double j);
double mul(double i, double j);
double exc(double i, double j);
}
实现类
[!IMPORTANT]
组件注册+组件扫描
AOP功能值针对IoC容器的对象(不是接口,是实现的对象)->所以要创建代理对象(不是对象)->存储到IoC容器
@Component
public class CanculateImpl implements Canculator {
@Override
public double add(double i, double j) {
return i + j;
}
@Override
public double sub(double i, double j) {
return i - j;
}
@Override
public double mul(double i, double j) {
return i * j;
}
@Override
public double exc(double i, double j) {
return i / j;
}
}
配置代理
创建代理对象,并且开启IoC扫描激昂对象加入到IoC容器
- 方式1:spring.xml
<context:component-scan base-package="需要被代理的对象类(需要加入IoC容器的组件)"/>
- 方式2:配置类
@Configuration //指定配置类
@ComponentScan("org.zhuhaihong.service") //扫描包
public class Config {
}
Spring 创建IoC容器
相当于一个普通的类,包括类的字段calculator,在类方法中使用自动装配的calculator调用方法.所有的类的自动装配都可以参考这个形式
@SpringJUnitConfig(value = Config.class) //根据IoC配置类加载IoC容器(xml配置的扫描使用location加载IoC配置类)空容器
public class SpringAopTest {
//被代理对象放入IoC容器
//private CalculateImpl canculate;
// todo:注意:有接口一定要用接口去创建?防止使用AOP去不到代理对象——到底是将谁放入IoC容器?——目前是正常业务代码
@Autowired //将对象自动注入:没有Aop时注入的时目标对象,有AOP时,系统自动注入代理对象
private Calculator calculator;
//测试
@Test //使用junit/junit测试空指针异常(无法自动转装配)
public void test() {
double add = this.calculator.add(3, 2);
double sub = this.calculator.sub(3, 2);
double mul = this.calculator.mul(3, 2);
double exc = this.calculator.exc(3, 2);
System.out.println("最终结果:" + add + "\t" + sub + "\t" + mul + "\t" + exc);
}
}
[!Note]
注意此处的自动注入:
[问题1]有接口一定要用接口去创建?防止使用AOP去不到代理对象——到底是将谁放入IoC容器?——目前是正常业务代码
[回答]
- 在AOP中,是将代理对象放入IoC而不是被代理对象;
- 此处不声明为被代理目标对象是因为目前还没有是有AOP,所以要直接调用被代理对象的方法就要将其放入IoC;
- 如果现在用接口声明,一会儿创建AOP后,需要回来该这个代码变成代理对象————所以直接使用接口
[总结]
- 将对象自动注入:没有Aop时注入目标对象,有AOP时,系统自动注入代理对像
- AOP注入:有接口的目标代理为接口,没接口的代理为子类————所以有接口声明接口,没接口声明目标类
[问题2] 测试中自动装配空指针异常(对象配有被自动装配)
[解决] 使用
junit-jupiter-api
中的测试注解
定义增强类
前置、返回、错误通知
- 实现目标:
public double add(double i, double j) { try { System.out.println("add bengin"); //前置 double v = i + j; //核心代码 System.out.println("add end"); //返回 }catch (Exception e){ System.out.println("error"); //错误 } return i+j; }
- 基于execution配置切点
/**
* <h1>增强类</h1>
* 1.定义增强方法存储增强代码(根据插入位置选定)<p>
* 2.使用注解指定插入目标方法的位置:<p>
* 前置{@link org.aspectj.lang.annotation.Before}<p>
* 后置{@link org.aspectj.lang.annotation.AfterReturning}<p>
* 异常{@link org.aspectj.lang.annotation.AfterThrowing}<p>
* 最后{@link org.aspectj.lang.annotation.After}<p>
* 环绕{@link org.aspectj.lang.annotation.Around}<p>
* 3.配置切点表达式(选中需要切入的连接点)--目前是在通知注解上配置<p>
* 方式1:execution配置——返回值类型 包名.类名(*).方法名(参数列表..)<p>
* 方式2:注解配置<p>
* 4.将增强类放入IoC容器,注解组件<p>
* 5.切面注解(只有注解了切面,才会自动生成代理对象,装配到代理对象字段)(切面=切点+增强)<p>
* [问题]那么目标对象还需要加入IoC容器吗?todo<p>
* [回答]一定要,AOP只会为IoC容器中的目标对象生成代理对象,只是在自动注入的时候,将代理对象自动注入到未使用AOP时的目标对象中(不需要修改原代码,前提是有接口的使用接口创建的对象)<p>
*
*/
@Component
@Aspect
public class LogAdvice {
//Before的value不能为空
@Before("execution(* org.zhuhaihong.service.impl.*.*(..))")//切点
public void begin() {
System.out.println("method begin");
}
@AfterReturning("execution(* org.zhuhaihong.service.impl.*.*(..))")
public void after() {
System.out.println("method end");
}
@AfterThrowing("execution(* org.zhuhaihong.service.impl.*.*(..))")
public void error() {
System.out.println("method error");
}
}
- 注解方式
- 注解定义
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LogAnnotation { String value() default ""; }
- 切点配置
@Component @Aspect public class LogAdvice { //Before的value不能为空 // @Before("execution(* org.zhuhaihong.service.impl.*.*(..))")//切点 @Before("@annotation(org.zhuhaihong.annotation.LogAnnotation)")//切点 public void begin() { System.out.println("method begin"); } // @AfterReturning("execution(* org.zhuhaihong.service.impl.*.*(..))") @AfterReturning("@annotation(org.zhuhaihong.annotation.LogAnnotation)") public void after() { System.out.println("method end"); } // @AfterThrowing("execution(* org.zhuhaihong.service.impl.*.*(..))") @AfterThrowing("@annotation(org.zhuhaihong.annotation.LogAnnotation)") public void error() { System.out.println("method error"); } }
配置Spring
[!important]
组件扫描+AOP自动代理功能的开启
config.Config.java
@ComponentScan("org.zhuhaihong") //要包含目标类组件和增强类组件
@EnableAspectJAutoProxy //开启自动代理(需要开启才能自动AOP代理)
public class Config {
}
-
xml方式
<aop:aspectj-autoproxy/>
切点的统一管理
这属于优化内容,由上面的切点配置可以看出,在每个通知前面都要增加切点表达式
[局限]
当连接点变化,或者切点选择规则变化时,需要在每一个通知中更改切点表达式
[优化]
增加一个专门的切点类,定义一个空方法,在方法上增加切点表达式,通知中的切点表达式直接引用该方法即可
相当于将选中的连接点都放入该空方法中,而增强只增强该空方法.
pointCut.PointCut.java
@Component
public class MyCutPoint {
// @Pointcut("@annotation(org.zhuhaihong.annotation.LogAnnotation)")
@Pointcut("execution(* org.zhuhaihong.service.impl.CalculateImpl(..))")
public void pt(){}
}
advice.LogAdvice.java
@Component
@Aspect
public class LogAdvice {
@Before("org.zhuhaihong.myCutPoint.MyCutPoint.pt()")
public void begin() {
System.out.println("method begin");
}
@AfterReturning("org.zhuhaihong.myCutPoint.MyCutPoint.pt()")
public void after() {
System.out.println("method end");
}
@AfterThrowing("org.zhuhaihong.myCutPoint.MyCutPoint.pt()")
public void error() {
System.out.println("method error");
}
}
[!Important]
- 切点类一定也要加入IoC容器(组件注册+组件扫描)
- 修改切点的时候,只需要修改切点类就行了
获取链接点信息
-
**[所有通知]**JointPoint接口——获取方法签名、传入的实参等信息
-
通过getSignature()获取目标方法的签名(声明时的完整信息)
-
可以获取调用目标方法传入的实参列表数组
[!NOTE]
说明在代理方法执行前,就能拿到目标对象的传入的实参
-
-
**[返回通知]**通过@AfterReturning注解的returning属性获取目标方法返回值,然后使用该值进行配置通知的参数列表中的返回值变量
-
**[异常通知]**通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象,然后使用该值进行配置通知的参数列表中的返回值变量
LogAdvice.java
@Component
@Aspect
public class LogAdvice {
@Before(value = "org.zhuhaihong.myCutPoint.MyCutPoint.pt()")
public void begin() {
System.out.println("method begin");
}
@AfterReturning(value = "org.zhuhaihong.myCutPoint.MyCutPoint.pt()",returning = "rt")
public void after(JoinPoint joinPoint,Object rt) {
System.out.println(joinPoint.getSignature()+"method end,The result:"+rt);
}
@AfterThrowing(value = "org.zhuhaihong.myCutPoint.MyCutPoint.pt()",throwing = "t")
public void error(JoinPoint joinPoint,Throwable t) {
System.out.println(joinPoint.getSignature()+"method error:"+t);
}
}
案例2:事务管理——环绕通知
JointPoint
和ProceedingJointPoint
- JointPoint——无执行方法
public interface JoinPoint {
String METHOD_EXECUTION = "method-execution";
String METHOD_CALL = "method-call";
String CONSTRUCTOR_EXECUTION = "constructor-execution";
String CONSTRUCTOR_CALL = "constructor-call";
String FIELD_GET = "field-get";
String FIELD_SET = "field-set";
String STATICINITIALIZATION = "staticinitialization";
String PREINITIALIZATION = "preinitialization";
String INITIALIZATION = "initialization";
String EXCEPTION_HANDLER = "exception-handler";
String SYNCHRONIZATION_LOCK = "lock";
String SYNCHRONIZATION_UNLOCK = "unlock";
String ADVICE_EXECUTION = "adviceexecution";
String toString();
String toShortString();
String toLongString();
Object getThis();
Object getTarget();
Object[] getArgs(); //获取连接点的传入的参数
Signature getSignature(); //连接点签名
SourceLocation getSourceLocation();
String getKind();
StaticPart getStaticPart();
//....
}
- ProceedinJointPoint——有执行方法
public interface ProceedingJoinPoint extends JoinPoint {
void set$AroundClosure(AroundClosure var1);
default void stack$AroundClosure(AroundClosure arc) {
throw new UnsupportedOperationException();
}
Object proceed() throws Throwable;
Object proceed(Object[] var1) throws Throwable;
}
通知一定要有返回值和参数
环绕通知增强
-
advice.TxAroundAdvice.java
@Component @Aspect public class TxAroundAdvice { // 环绕通知(必须要求返回值和参数)——需要在增强中定义目标方法的执行,原始通知在通知中也可以添加JointPoint——只能获取目标方法的值,不能执行(也不需要),但是环绕通知必须定义目标方法在何时执行 /** * 环绕增强 * */ @Around("org.zhuhaihong.myCutPoint.MyCutPoint.pt()") public Object transaction(ProceedingJoinPoint joinPoint) { //目标方法执行参数准备 Object[] args = joinPoint.getArgs(); //接受目标方法执行返回结果 Object result = null; //测试最终返回的时目标对象的返回值还是经过增强处理之后的返回值 Double re; //增强——前置,后置、错误、最终 try { System.out.println("tx open"); //目标方法执行 result = joinPoint.proceed(args); re = (Double) result; re += 1; System.out.println("tx commit"); } catch (Throwable e) { System.out.println("tx rollback"); throw new RuntimeException(e); } finally { System.out.println("tx close"); } return re; }
测试
SpringAOPTest.java
@SpringJUnitConfig(value = Config.class)
public class SpringAopTest
@Autowired
private Calculator calculator;
//测试
@Test
public void test(){
double exc = this.calculator.exc(3, 2);
System.out.println(exc);
//tx open
//tx commit
//tx close
//2.5
}
}
[!IMPORTANT]
- 环绕通知必须要有传入参数:ProceedingJointPoint——指定连接点
- 最后经过代理对象调用后,最后返回的是代理对象的返回值.
[理解]
- 其实就相当于普通函数调用其他函数,其他函数的返回值被本函数调用接收,当前函数继续处理并返回.印证调用的是代理对象的方法.
- 对于前置、后置、异常、最终通知,也是调用的代理对象的方法,没有涉及返回值,则返回目标函数的返回值.
- 环绕通知一定要通过try-catch结构,并且通过
throw
抛出异常,外部才能得到该异常信息.
切面优先级(多层代理)*
[场景]
相同目标方法上同时存在多个切面时,切面的优先级控制面的内外嵌套顺序。
•优先级高的切面:外面
•优先级低的切面:里面
[解决方案]
使用
@Order
注解,控制切面优先级
@Order
越小,优先级越高
[!caution]
注意不是完全的先后执行,而是关注目标方法前还是目标方法后,一种嵌套的感觉
优先级越高——外层
[总结]
-
AOP只会为IoC容器中的目标对对象创建代理对象,所以目标对象一定要放在IoC容器中.
-
AOP的逻辑是为IoC容器中指定的对象(切点选中)创建代理对象,利用多态的性质,通过生成同一接口对象(JDK)/目标子类(cglib)的代理对象,然后在自动注入的时候,注入对象中的就不是原目标对象,而是代理对象(多态的性质:接口/目标类可以同时表示目标对象和代理对象,这一不用修改代码)————所以实际是代理对象在执行
-
不要忘记组件扫描增强类也需要加入IoC,并且需要使用@Aspect表示切面(可以想像称方法模版,一个类就是一个模版),只有配置了注解的才会为切点相关类(目标对象),创建代理对象
-
不要忘记开启自动代理
@EnableAspectJAutoProxy
只有在配置类中开启了. -
需要放入IoC的类:代理目标类、切面类、切点管理类
-
环绕通知可以对连接点的返回值进一步处理,并且返回代理方法的返回值
-
切入点参数列表的类型——说明在代理执行前,代理对象就能拿到传入的实参
目标方法 返回值 异常 JointPoint/ProceedingJointPoint Object Throwable -
使用AOP后,对Bean的自动注入的对象是有影响的,因此要注意装配的事代理对象还是目标对象
问题
-
AOP的嵌套?
[回答]使用
@Order
注解指定,值越小,越外层.(如果有多个切面,不指定@Order
则会根据加载(组件扫描)顺序执行)