一、什么是AOP
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。简单来说就是可以在一个程序运行的各个阶段插入新功能,而不用修改原代码。
举个例子:
我们有A,B两个服务,现在需要给所有服务运行前后添加日志模块,实现方法有很多
一种方法是写一个工具类,然后在服务中引入工具类,源代码前后添加工具类调用,这样耦合度很高,不利于后续的扩展
最好的方法就是使用动态代理为原代码扩充新功能
https://blog.youkuaiyun.com/qq_33967820/article/details/119317255
而aop就是基于动态代理来实现操作的,spring中常用AspectJ来实现
二、AspectJ的设计理念
Aspect 切面,表示增强的功能,一个程序完成某个非业务功能,常见的切面功能有日志,事务,权限检查等;
JoinPoint 连接点,链接业务方法和切面的未知
Advice 表示切面执行的时间
Pointcut 这是一组一个或多个连接点,通知应该被执行。你可以使用表达式或模式指定切入点正如我们将在 AOP 的例子中看到的。
Introduction 引用允许你添加新方法或属性到现有的类中。
Target object 被一个或者多个方面所通知的对象,这个对象永远是一个被代理对象。也称为被通知对象。
Weaving Weaving 把方面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时,类加载时和运行时完成。
三、Advice和Pointcut
5种advice通知
Before前置通知 在一个方法执行之前,执行通知。
After后置通知 在一个方法执行之后,不考虑其结果,执行通知。
AfterReturning返回后通知 在一个方法执行之后,只有在方法成功完成时,才能执行通知。
AfterThrowing抛出异常后通知 在一个方法执行之后,只有在方法退出抛出异常时,才能执行通知。
Around环绕通知 在建议方法调用之前和之后,执行通知。
Pointcut写法
访问修饰符 方法返回值 包名.类名.方法声明(参数) 异常类型
**其中访问修饰符,包名.类名,异常类型可以省略**
*表示任意多个字符(包括0个)
..用在方法参数中表示任意多个参数,用在包名后表示当前包以及子包路径
+用在类名后,表示当前类以及子类,用在接口后表示当前接口以及实现类
1)execution(* *(..))
//表示匹配所有方法
2)execution(public * com. fssx.service.UserService.*(..))
//表示匹配com.fssx.server.UserService中所有的公有方法
3)execution(* com.fssx.server..*.*(..))
//表示匹配com.fssx.server包及其子包下的所有方法
四、AspectJ使用
在maven的pom文件中添加spring-context和spring-aspects依赖
1、使用xml文件配置
创建切面类
package com.fssx.aspectj;
import com.fssx.domain.Student;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAspectJ {
//before通知触发,程序运行之前,around通知之后
public void Before(){
System.out.println("before通知触发");
}
//after通知触发,程序运行之后、around通知运行后
public void After(){
System.out.println("after通知触发");
}
// 后置通知,在方法之后执行,能够捕捉目标方法的返回值,根据返回值做处理
// 因为切面执行的时候,程序已经返回,切面拿到的只是返回值的引用,所以可以修改引用类型,不可以修改基本类型
public void AfterReturning(Object res){
Student student = (Student) res;
System.out.println("afterReturn通知触发");
if(student != null){
System.out.println("返回值为"+student);
student.setName("王二麻子");
}
}
// 异常通知,会在异常发生时触发
public void AfterThrowing(Exception ex){
System.out.println("异常错误");
}
// 环绕通知,可以在程序运行前,程序运行后,通过ProceedingJoinPoint.proceed()调用服务
public Object Around(ProceedingJoinPoint pjp){
Object res = null;
System.out.println("环绕通知开始");
try {
res = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知结束");
return res;
}
}
xml文件配置
<!--将切面类加入Spring容器-->
<bean id="aspect1" class="com.fssx.aspectj.MyAspectJ"/>
<aop:config>
<aop:aspect id="myAspect" ref="aspect1">
<!--设置切入点-->
<aop:pointcut id="pointcut" expression="execution(* *(..))"/>
<!--before通知触发,程序运行之前,around通知之后-->
<aop:before method="Before" pointcut-ref="pointcut"/>
<!--after通知触发,程序运行之后、around通知运行后-->
<aop:after method="After" pointcut-ref="pointcut"/>
<!--后置通知,在方法之后执行,能够捕捉目标方法的返回值,根据返回值做处理
因为切面执行的时候,程序已经返回,切面拿到的只是返回值的引用,
所以可以修改引用类型,不可以修改基本类型-->
<aop:after-returning method="AfterReturning"
pointcut-ref="pointcut" returning="res" />
<!--异常通知,会在异常发生时触发-->
<aop:after-throwing method="AfterThrowing"
pointcut-ref="pointcut" throwing="ex"/>
<!--环绕通知,可以在程序运行前后,
通过ProceedingJoinPoint.proceed()调用服务-->
<aop:around method="Around" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
测试类
public void Test1()
{
String config = "spring.xml";
ApplicationContext context= new ClassPathXmlApplicationContext(config);
MyService service = (MyService) context.getBean("service1");
Student res = (Student) service.doSome();
System.out.println("最终返回值为"+res);
}
运行结果
before通知触发
环绕通知开始
doSome
环绕通知结束
afterReturn通知触发
返回值为Student{name='李四', sex='男', age=22}
after通知触发
最终返回值为Student{name='王二麻子', sex='男', age=22}
2、使用注解配置
创建切面类
package com.fssx.aspectj;
import com.fssx.domain.Student;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
//注册切面类
@Aspect
public class MyAspectJ2 {
// 创建切入点,和上文中的配置规则相同
@Pointcut("execution(* *(..))")
public void setPointcut(){}
// 两种方式配置切入点,1是使用value值赋值,value可以省略,2是使用创建好的切入点方法
//before通知触发,程序运行之前,around通知之后
@Before("setPointcut()")
public void Before(){
System.out.println("before通知触发");
}
//after通知触发,程序运行之后、around通知运行后
@After("setPointcut()")
public void After(){
System.out.println("after通知触发");
}
// 后置通知,在方法之后执行,能够捕捉目标方法的返回值,根据返回值做处理
// 因为切面执行的时候,程序已经返回,切面拿到的只是返回值的引用,所以可以修改引用类型,不可以修改基本类型
@AfterReturning(value = "setPointcut()", returning = "res")
public void AfterReturning(Object res){
Student student = (Student) res;
System.out.println("afterReturn通知触发");
if(student != null){
System.out.println("返回值为"+student);
student.setName("王二麻子");
}
}
// 异常通知,会在异常发生时触发
@AfterThrowing(value = "setPointcut()",throwing = "ex")
public void AfterThrowing(Exception ex){
System.out.println("异常错误");
}
// 环绕通知,可以在程序运行前,程序运行后,通过ProceedingJoinPoint.proceed()调用服务
@Around("setPointcut()")
public Object Around(ProceedingJoinPoint pjp){
Object res = null;
System.out.println("环绕通知开始");
try {
res = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("环绕通知结束");
return res;
}
}
xml文件配置
<!--将切面类加入Spring容器-->
<bean id="aspect2" class="com.fssx.aspectj.MyAspectJ2"/>
<!--声明自动代理生成器,会把spring中的所有的目标对象,一次性生成代理对象-->
<aop:aspectj-autoproxy/>
测试类
public void Test1()
{
String config = "spring.xml";
ApplicationContext context= new ClassPathXmlApplicationContext(config);
MyService service = (MyService) context.getBean("service1");
Student res = (Student) service.doSome();
System.out.println("最终返回值为"+res);
}
运行结果
环绕通知开始
before通知触发
doSome
环绕通知结束
after通知触发
afterReturn通知触发
返回值为Student{name='李四', sex='男', age=22}
最终返回值为Student{name='王二麻子', sex='男', age=22}
xml和注解两种运行结果不一致
原因未知,代解决
三、其他
如果目标类有接口的话,aspectj会自动使用jdk动态代理实现,如果没有接口就会使用cglib
也可以添加
xml可以<aop:config proxy-target-class=“true”>
注解可以<aop:aspectj-autoproxy proxy-target-class=“true”/>
让框架在有接口的情况下也使用cglib