Spring AOP 实现

AOP(Aspect Orient Programming),我们一般称为面向切面编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务、日志、缓存、分布式锁等等。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。Spring的主要动态代理有CGLib和JDK自动代理。

使用AspectJ的编译时增强实现AOP

AspectJ是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。

编译成字节码.class比原来的.java会多了一些代码,这就是AspectJ的静态代理,它会在编译阶段将Aspect织入Java字节码中, 运行的时候就是经过增强之后的AOP对象。

使用Spring AOP

与AspectJ的静态代理不同,Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

现在我们做一个测试:

首先定义一个接口:

1package cn.chinotan.service; 2 3/** 4 * @program: test 5 * @description: 动物 6 * @author: xingcheng 7 **/ 8public interface Animal { 9 10 /** 11 * 跑 12 * @param where 在什么地方跑 13 * @return 14 */ 15 String run (String where); 16 17}

其实现类:

1package cn.chinotan.service.impl; 2 3import cn.chinotan.aop.Action; 4import cn.chinotan.service.Animal; 5import org.springframework.aop.framework.AopContext; 6import org.springframework.stereotype.Service; 7 8/** 9 * @program: test 10 * @description: 狗 11 * @author: xingcheng 12 **/ 13@Service 14public class Dog implements Animal { 15 16 @Action 17 @Override 18 public String run(String where) { 19 System.out.println("狗往" + where + "跑"); 20 return "地点是:" + where; 21 } 22}

其中@Action为自定义的注解,用来指定aop代理的切入点

1package cn.chinotan.aop; 2 3import java.lang.annotation.ElementType; 4import java.lang.annotation.Retention; 5import java.lang.annotation.RetentionPolicy; 6import java.lang.annotation.Target; 7 8/** 9 * @program: test 10 * @description: 动作 11 * @author: xingcheng 12 **/ 13@Target(ElementType.METHOD) 14public @interface Action { 15 16}

定义Aspect:

1package cn.chinotan.aspect; 2 3import org.aspectj.lang.annotation.Aspect; 4import org.aspectj.lang.annotation.Before; 5import org.aspectj.lang.annotation.Pointcut; 6import org.springframework.stereotype.Component; 7 8/** 9 * @program: test 10 * @description: 动作aop实现 11 * @author: xingcheng 12 **/ 13@Aspect 14@Component 15public class ActionAspect { 16 17 @Pointcut("@annotation(cn.chinotan.aop.Action)") 18 void actionPointCut() { 19 } 20 21 @Before("actionPointCut()") 22 void beforeAction() { 23 System.out.println("热身运动"); 24 } 25}

其中有几种aop的通知注解:

  1. @Before: 前置通知, 在方法执行之前执行
  2. @After: 后置通知, 在方法执行之后执行
  3. @AfterRunning:返回通知, 在方法成功执行返回结果之后执行
  4. @AfterThrowing: 异常通知, 在方法抛出异常之后
  5. @Around: 环绕通知,围绕着方法执行

@Pointcut是切入点的注解:

这里使用了@annotation 可以在使用了自定义注解的配置方法上实现切入

也可以使用execution(* *(..))的形式:

    声明切入点
    第一个*表示 方法  返回值(例如public int)
    第二个* 表示方法的全限定名(即包名+类名)
    perform表示目标方法参数括号两个.表示任意类型参数
    方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,
    我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么
    execution表示执行的时候触发

在启动的application.yml配置文件中加入

spring.aop.proxy-target-class: false

这个是控制aop的具体实现方式,为true 的话使用cglib,为false的话使用java的Proxy,默认是false

之后运行controller:

1package cn.chinotan.controller; 2 3import cn.chinotan.service.Animal; 4import org.springframework.beans.factory.annotation.Autowired; 5import org.springframework.web.bind.annotation.GetMapping; 6import org.springframework.web.bind.annotation.RequestMapping; 7import org.springframework.web.bind.annotation.RestController; 8 9/** 10 * @program: test 11 * @description: test类 12 * @author: xingcheng 13 **/ 14@RestController 15@RequestMapping("/test") 16public class TestController { 17 18 @Autowired 19 Animal animal; 20 21 @GetMapping("/aopRun") 22 public String aopRun() { 23 animal.run("狗窝"); 24 System.out.println("dog代理为:" + animal.getClass()); 25 26 return "ok"; 27 } 28 29}

打印日志:

可以看到类型是com.sun.proxy.$Proxy71,也就是前面提到的Proxy类,因此这里Spring AOP使用了JDK的动态代理。

再来看看不实现接口的情况,修改Dog类:

配置proxy-target-class: false依旧
 

1package cn.chinotan.service.impl; 2 3import cn.chinotan.aop.Action; 4import cn.chinotan.service.Animal; 5import org.springframework.aop.framework.AopContext; 6import org.springframework.stereotype.Service; 7 8/** 9 * @program: test 10 * @description: 狗 11 * @author: xingcheng 12 **/ 13@Service 14public class Dog { 15 16 @Action 17 public String run(String where) { 18 System.out.println("狗往" + where + "跑"); 19 return "地点是:" + where; 20 } 21}

打印日志:

可以看到类被CGLIB增强了,也就是动态代理。这里的CGLIB代理就是Spring AOP的代理,这个类也就是所谓的AOP代理,AOP代理类在切点动态地织入了增强处理。

可以看到:

    AspectJ在编译时就增强了目标对象,Spring AOP的动态代理则是在每次运行时动态的增强,生成AOP代理对象,区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

    java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,如果目标对象实现了接口,可以强制使用CGLIB实现AOP,如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

误区注意:

在平时开发中,我们通常在Service中定义了一个方法并且切入之后,从Controller里面调用该方法可以实现切入,但是当在同一个Service中实现另一方法并调用改方法时却无法切入

类似于:

1package cn.chinotan.service.impl; 2 3import cn.chinotan.aop.Action; 4import cn.chinotan.service.Animal; 5import org.springframework.aop.framework.AopContext; 6import org.springframework.stereotype.Service; 7 8/** 9 * @program: test 10 * @description: 狗 11 * @author: xingcheng 12 * @create: 2018-10-27 16:00 13 **/ 14@Service 15public class Dog implements Animal { 16 17 @Action 18 @Override 19 public String run(String where) { 20 System.out.println("狗往" + where + "跑"); 21 return "地点是:" + where; 22 } 23 24 @Override 25 public void runToEat(String food) { 26 run("狗窝"); 27 System.out.println("狗在吃" + food); 28 } 29} 30 31 32package cn.chinotan.service.impl; 33 34import cn.chinotan.aop.Action; 35import cn.chinotan.service.Animal; 36import org.springframework.aop.framework.AopContext; 37import org.springframework.stereotype.Service; 38 39/** 40 * @program: test 41 * @description: 狗 42 * @author: xingcheng 43 * @create: 2018-10-27 16:00 44 **/ 45@Service 46public class Dog implements Animal { 47 48 @Action 49 @Override 50 public String run(String where) { 51 System.out.println("狗往" + where + "跑"); 52 return "地点是:" + where; 53 } 54 55 @Override 56 public void runToEat(String food) { 57 run("狗窝"); 58 System.out.println("狗在吃" + food); 59 } 60}

我们在执行runToEat方法时,调用了自己类中的另一个方法,结果为:

可以看到run()的切面方法并没有执行,以上结果的出现与Spring AOP的实现原理息息相关,由于Spring AOP采用了动态代理实现AOP,在Spring容器中的bean(也就是目标对象)会被代理对象代替,代理对象里加入了我们需要的增强逻辑,当调用代理对象的方法时,目标对象的方法就会被拦截,

通过调用代理对象的action方法,在其内部会经过切面增强,然后方法被发射到目标对象,在目标对象上执行原有逻辑,如果在原有逻辑中嵌套调用了work方法,则此时work方法并没有被进行切面增强,因为此时它已经在目标对象内部。而解决方案很好地说明了,将嵌套方法发射到代理对象,这样就完成了切面增强。可以看下源码:

在代码3处,如果配置了exposeProxy开关,则会将代理对象暴露在当前线程中,以供其它需要的地方使用,通过使用静态的全局ThreadLocal变量就解决了问题。

spring提供了一个这样的类:

可以看到他可以获取到当前的aop代理,但是在获取之前,得开启exposeProxy开关

@EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true)

这样就可以进行代理了,打印日志为:

既然这样可以,那是不是直接applicationContext.getBean()也可以呢?实验过后得到的结果是可行,而且配置中的expose-proxy也不用设置成true,那试一下:

1package cn.chinotan.service.impl; 2 3import cn.chinotan.aop.Action; 4import cn.chinotan.service.Animal; 5import org.springframework.aop.framework.AopContext; 6import org.springframework.beans.factory.annotation.Autowired; 7import org.springframework.context.ApplicationContext; 8import org.springframework.stereotype.Service; 9 10/** 11 * @program: test 12 * @description: 狗 13 * @author: xingcheng 14 * @create: 2018-10-27 16:00 15 **/ 16@Service 17public class Dog implements Animal { 18 19 @Autowired 20 ApplicationContext applicationContext; 21 22 @Action 23 @Override 24 public String run(String where) { 25 System.out.println("狗往" + where + "跑"); 26 return "地点是:" + where; 27 } 28 29 @Override 30 public void runToEat(String food) { 31// Dog dog = (Dog) AopContext.currentProxy(); 32 Dog dog = (Dog) applicationContext.getBean("dog"); 33 dog.run("狗窝"); 34 System.out.println("狗在吃" + food); 35 } 36}

打印日志为:

可见同样可以

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )