AOP
1. 什么是AOP
AOP:能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
2. AOP常用注解
@Component
@Aspect //@Aspect 表示它是一个切面。
public class DemoAspect {
//配置切入点
@Pointcut("execution(* com.aop.service.*.*(..))")
public void myPointCut () {
}
//配置前置通知
@Before("myPointCut()")
public void myBefore () {
System.out.println("——————前置通知————");
}
//*returning为连接点的返回值,命名须与myAfterReturning()方法的Object参数一致。
@AfterReturning(pointcut = "myPointCut()", returning = "obj")
public void myAfterReturning (JoinPoint point, Object obj) {
System.out.println("——————返回通知————");
//获取连接点方法传入的实参
Object[] args = point.getArgs();
//获取连接点方法的方法名
String methodName = point.getSignature().getName();
//获取连接点方法所在的对象
Object targetObj = point.getTarget();
String targetClassName = point.getClass().getName();
}
@AfterThrowing(pointcut = "myPointCut()", throwing = "eeeee")
public void myAfterThrowing (JoinPoint point, Exception eeeee) {
//注意那串eeeee了吗,那是为了引起你的注意,告诉你这两个命名须一致。
System.out.println("——————异常通知————" + eeeee.getMessage());
}
@After("myPointCut()")
public void myAfter (JoinPoint point) {
System.out.println("——————最终通知————" + point.getSignature().getName());
}
}
3.Spring4各种通知的执行节点
*如果抛出异常,那@Around方法的后半部分就应该是不会执行的
4. 在aop中校验不通过如何不让程序进入核心代码?
通过aop中注解的执行的先后顺序我们知道,校验发生在核心代码前面的只剩下两个——@Before,@Around。
@Before : 这个注解只有在异常时才不会走核心方法——连接点。正常@Before无法阻止当前线程进入连接点。
@Around : 这个注解在连接点前后执行。并且注解的方法传入的ProceedingJionPoint 类中封装的代理方法proceed()可以让当前线程从aop方法转到连接点——核心代码方法。所以一般我们用这个注解,如果aop的安全校验不通过,则不调用proceed()方法,就永远不会进入连接点。
除此外,要注意除了Around注解的方法可以传ProceedingJionPoint 外,别的几个都不能传这个类。
5. 同一个方法被多个Aspect类拦截
aspect1 和 aspect2 的执行顺序是未知的。
5.1 如何指定每个 aspect 的执行顺序呢?
给aspect添加@Order注解,该注解全称为:org.springframework.core.annotation.Order
@Order(5)
@Component
@Aspect
public class Aspect1 {
// ...
}
@Order(6)
@Component
@Aspect
public class Aspect2 {
// ...
}
这样修改之后,可保证不管在任何情况下, aspect1 中的 advice 总是比 aspect2 中的 advice 先执行。如下图所示:
注意:
-
如果在同一个 aspect 类中,针对同一个 pointcut,定义了两个相同的 advice(比如,定义了两个@Before),那么这两个 advice 的执行顺序是无法确定的,哪怕你给这两个 advice 添加了@Order这个注解,也不行。这点切记。
-
对于@Around这个advice,不管它有没有返回值,但是必须要方法内部,调用一下pjp.proceed();否则,Controller 中的接口将没有机会被执行,从而也导致了
@Before这个advice不会被触发。比如,我们假设正常情况下,执行顺序为”aspect2 -> apsect1 ->controller”,如果,我们把 aspect1中的@Around中的
pjp.proceed();给删掉,那么,我们看到的输出结果将是:
6. AOP应用:
6.1 创建目标对象类
public interface IService {
String update(String msg);
}
@Service
public class IServiceImpl implements IService {
@Override
public String update(int uid, String msg) {
System.out.println("serviceimpl运行了");
return "更新了数据:"+msg;
}
}
6.2 切面
@Component
@Aspect //@Aspect 表示它是一个切面。
public class DemoAspect {
//配置切入点
@Pointcut("execution(* com.aop.service.*.*(..))")
public void myPointCut() {}
//配置前置通知
@Before("myPointCut()")
public void myBefore() {
System.out.println("——————前置通知————");
}
}
@Pointcut("execution("result range")
Pointcut注解有两个参数,result是返回值的类型 , range是指定范围来配置通知的类,两个参数用空格隔开
在演示代码中,我是用的 * com.aop.service..(…)),意义如下。
@Pointcut("execution(任意返回值 com包.aop包.service包.所有类.所有方法(任意参数))")
//其中返回值、包、类、方法的通配符为 *
//参数的通配符为 ..
6.3 测试前置通知
public class Test {
public static void main(String[] args) {
ApplicationContext ac=new ClassPathXmlApplicationContext("applicationContext.xml");
IService ser=ac.getBean(IService.class);
//update方法只是返回了这段字符串,所以并不会打印。
ser.update("测试字符串");
}
}
6.4 返回通知、异常通知、后置通知
//*returning为连接点的返回值,命名须与myAfterReturning()方法的Object参数一致。
@AfterReturning(pointcut="myPointCut()",returning="obj")
public void myAfterReturning(JoinPoint point,Object obj) {
System.out.println("——————返回通知————");
//获取连接点方法传入的实参
Object[] args=point.getArgs();
//获取连接点方法的方法名
String methodName=point.getSignature().getName();
//获取连接点方法所在的对象
Object targetObj=point.getTarget();
String targetClassName=point.getClass().getName();
}
@AfterThrowing(pointcut="myPointCut()",throwing="eeeee")
public void myAfterThrowing(JoinPoint point,Exception eeeee) {
//注意那串eeeee了吗,那是为了引起你的注意,告诉你这两个命名须一致。
System.out.println("——————异常通知————"+eeeee.getMessage());
}
@After("myPointCut()")
public void myAfter(JoinPoint point) {
System.out.println("——————最终通知————"+point.getSignature().getName());
}
spring5执行结果:
6.5 环绕通知
*环绕通知的返回值必须是Object,形参必须是ProceedingJoingPoint。
@Component
@Aspect
public class DemoAspect {
//配置切入点
@Pointcut("execution(* com.aop.service.*.*(..))")
public void myPointCut() {}
//环绕通知
@Around("myPointCut()")
public Object aroundAdvice(ProceedingJoinPoint proceeding) {
//和JoinPoint一样,ProceedingJoinPoint也可以获取
//连接点方法的实参
Object[] args=proceeding.getArgs();
//连接点方法的方法名
String methodName=proceeding.getSignature().getName();
//连接点方法所在的对象
Object targetObj=proceeding.getTarget();
String targetClassName=targetObj.getClass().getName();
Object result=null;
try {
System.out.println("前置通知====");
//执行连接点的方法 获取返回值
result=proceeding.proceed(args);
System.out.println("返回通知====");
}catch (Throwable e) {
System.out.println("异常通知===");
}finally {
System.out.println("最终通知===");
}
return result;
}
Spring5运行结果:
7. Spring4和Spring5的区别:
Springboot1.X底层应用的是Spring4
Springboot2.X底层应用的是Spring5
当系统应用升级,应该考虑一下以下变更。
8.动态代理静态代理
对静态代理、JDK动态代理、CGLib动态代理做一个总结,静态代理的维护成本比较高,有一个被代理类就需要创建一个代理类,而且需要实现相同的接口。JDK动态代理模式和CGLib动态代理的区别是JDK动态代理需要被代理类实现接口,而CGLib则是生成被代理类的子类,要求被代理类不能是final的,因为final类无法被继承。
动态代理总结:https://www.cnblogs.com/teach/p/10763845.html
参考链接:https://blog.csdn.net/rainbow702/article/details/52185827
参考链接:https://blog.csdn.net/zhanglf02/article/details/78132304
示例参考:https://blog.csdn.net/saltsoul/article/details/93140707
更多详细案例:https://www.cnblogs.com/joy99/p/10941543.html