• 【Java开发】 Spring 07 :Spring AOP 实践详解(通过 AOP 打印数据访问层)


    AOP 指是面向切面编程(通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术),利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率,阐述完 JDK 动态代理和 CGLIB 动态代理后将通过 AOP 技术打印数据访问层。

    目录

    1 AOP 实现原理

    1.1 AOP 实现

    1.2 Spring AOP 核心概念及注释

    ① 核心概念

    ② 常用注解

    1.3 Spring AOP 动态代理

    ① JDK 动态代理

    ② CGLIB 动态代理

    ③ 基于 xml 配置(以 JDK 动态代理为例)

    2 通过 AOP 打印数据访问层摘要(以 mybatis-plus 的 mapper 为例)

    2.1 项目初始化

    ① 导入 mybatis-plus 依赖

    ② 连接数据库配置

    ③ 编写实体类

    2.2 AOP 具体代码

    ① 编写 mapper 接口

    ② 编写 service 实现类

    ③ 编写 Aspect 类

    ④ 编写测试类

    ⑤ 输出结果

    参考文章


    项目源码:尹煜 / AopDemo · GitCode

    1 AOP 实现原理

    1.1 AOP 实现

    AOP代表的是一个横向的关系,将“对象”比作一个空心的圆柱体,其中封装的是对象的属性和行为;则面向方面编程的方法,就是将这个圆柱体以切面形式剖开,选择性的提供业务逻辑。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹,但完成了效果。

    简单来说,AOP 可以在不改变源码的前提下通过创建类的形式给某个方法添加新的功能。比如一个只能计算加法的方法,它想增加计算乘法的功能,但是又不想改源码!这时候可以在外面创建一个类,在这个类中通过AOP的技术给这个方法进行添加功能的操作,这就是AOP。

    AOP 可用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等等。

    AOP实现主要分为👇

    • 静态代理AspectJ 技术,是引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
    • 动态代理Spring AOP 技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行。

    由于本文专注于 Spring AOP ,也就是所谓的动态代理,因此不对静态代理做过多叙述,感兴趣的可以自行百度哈,然后介绍一下Spring AOP 的核心概念及注释,初次接触这些概念可能会有点陌生,不妨敲完后边的举例代码再回头来理解一下 👇

    1.2 Spring AOP 核心概念及注释

    ① 核心概念

    概念含义
    Aspect切面,切面是一个动作,也叫一个过程
    Join Point连接点是在应用执行过程中能够插入切面(Aspect)的一个点,简单来说是可以被增强的方法
    Advice通知,在连接点执行的动作,包括前通知、后通知、返回后通知、异常通知和环绕通知
    Pointcut切入点,说明如何匹配连接点
    Introduction引入,为现有类型声明额外的方法和属性
    Target object目标对象
    AOP proxyAOP 代理对象,可以是 JDK 动态代理,也可以是 CGLIB 代理
    Weaving 织入,连接切面与目标对象或类型创建代理的过程

    ② 常用注解

    • @EnableAspectJAutoProxy:开启aspectj的支持,对打@aspectj相关注解的类做一个Proxy
    • @Aspect:用来声明当前这个类是一个切面,需要说明的是光加aspect还不能变成一个bean,要么使用Javaconfig将类声明成一个bean,要么增加一个@Component注解来将这个bean创建出来
    • @Pointcut:用来指定Pointcut,具体在代码中体现
    • @Before:用来指定advice是在方法执行前是执行的
    • @After / @AfterReturning / @AfterThrowing:不管什么情况,在你结束了之后执行;在你成功返回之后执行;在你报错的时候执行
    • @Around:把这个方法前后都可以处理,把代码封装到里面
    • @Order;指定切面的一个执行顺序,order越小,优先级越高

    1.3 Spring AOP 动态代理

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

    动态代理形式主要分为两种:JDK动态代理 和 CGLIB动态代理

    首先需要在 pom.xml 文件中导入 aspectjweaver 依赖 👇

    1. <dependency>
    2. <groupId>org.aspectjgroupId>
    3. <artifactId>aspectjweaverartifactId>
    4. <version>1.9.2version>
    5. dependency>

    ① JDK 动态代理

    如果目标类实现接口,那么 Spring AOP 会默认选择使用 JDK 来动态代理目标类(若默认:proxy-target-class = false),不过本项目情况不大一样,请往下看~

    JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口,也就是说 JDK 动态代理主要用于接口的 AOP 实现,用于给接口(实现该接口的类也会被增强)增加功能。

    核心类包括 👇

    • InvocationHandler 接口
    • Proxy.newProxyInstance()

    Ⅰ 定义一个 Chinese 接口 :

    1. package com.aopdemo.ajdkDynamicProxy;
    2. public interface Chinese {
    3. //定义一个 sayHello 的抽象方法
    4. String sayHello(String name);
    5. }

    Ⅱ 实现类–Yinyu:

    Yinyu 类实现 Chinese 接口,并重写 sayHello 这个抽象方法

    1. package com.aopdemo.ajdkDynamicProxy;
    2. @Component //实例化到spring容器中,相当于配置文件中的
    3. public class Yinyu implements Chinese{
    4. @Override
    5. public void sayHello(String name) {
    6. System.out.println(name + "hello, AOP");
    7. }
    8. public void eat(String food) {
    9. System.out.println("我正在吃:" + food);
    10. }
    11. }

    Ⅲ 定义Aspect

    1. package com.aopdemo.ajdkDynamicProxy;
    2. @Aspect
    3. @Component
    4. public class DynamicProxyAdvice {
    5. //针对 Yinyu 类下的 sayHello 方法进行切入
    6. @Pointcut("execution(* com.aopdemo.ajdkDynamicProxy.Yinyu.sayHello(..))")
    7. public void pointcut() {
    8. }
    9. //接入点的前置操作
    10. @Before("pointcut()")
    11. public void before() {
    12. System.out.println("before");
    13. }
    14. }

    Ⅳ 使用测试类进行测试

    1. package com.aopdemo;
    2. @SpringBootTest
    3. class AopdemoTests {
    4. @Autowired
    5. private Chinese chinese;
    6. @Test
    7. void ajdkDynamicProxyTest() {
    8. chinese.sayHello("yinyu say:");
    9. System.out.println(chinese.getClass());
    10. }
    11. }

    Ⅴ 配置 properties + 输出结果

    注意:本项目若不按以下配置,那么将采用 CGLIB 动态代理,按理说 Yinyu 对象实现了接口,默认情况下会采用 JDK 的动态代理实现 AOP(同时默认情况下 spring.aop.proxy-target-class = false),但我实践的话并非如此,因此可以得出默认情况下proxy-target-class="true"(如果目标对象实现了接口,可以强制使用CGLIB实现AOP),至少我这个项目是这样。

    所以我将 proxy-target-class 配置成了 false ,如果大家不需要此配置也可输出 $Proxy61 的话就不用这个配置。

    路径:src/main/resources/application.properties

    spring.aop.proxy-target-class = false

    输出结果 👇

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

    ② CGLIB 动态代理

    如果目标类没有实现接口,那么 Spring AOP 会默认选择使用 CGLIB 来动态代理目标类;若目标对象实现了接口,也可以强制使用 CGLIB 实现 AOP(设置 proxy-target-class = true)

    CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,CGLIB 是通过继承的方式做动态代理,因此如果某个类被标记为 final,那么它是无法使用CGLIB做动态代理的。

    核心类包括 👇

    • MethodInterceptor 接口
    • Enhancer 类

    Ⅰ定义类 CGLIBYinyu

    定义一个不实现任何接口的类

    1. package com.aopdemo.aCGLIBDynamicProxy;
    2. @Component //实例化到spring容器中,相当于配置文件中的
    3. public class CGLIBYinyu{
    4. public void eat(String food) {
    5. System.out.println("我正在吃:" + food);
    6. }
    7. }

    Ⅱ 定义Aspect

    1. package com.aopdemo.aCGLIBDynamicProxy;
    2. @Aspect
    3. @Component
    4. public class CGLIBAdvice {
    5. //针对 CGLIBYinyu 类下的 eat 方法进行切入
    6. @Pointcut("execution(* com.aopdemo.aCGLIBDynamicProxy.CGLIBYinyu.eat(..))")
    7. public void pointcut() {
    8. }
    9. //接入点的前置操作
    10. @Before("pointcut()")
    11. public void before() {
    12. System.out.println("before");
    13. }
    14. }

    Ⅲ 使用测试类进行测试

    1. package com.aopdemo;
    2. @SpringBootTest
    3. class AopdemoTests {
    4. @Autowired
    5. private CGLIBYinyu cglibYinyu;
    6. @Test
    7. void ajdkDynamicProxyTest() {
    8. chinese.sayHello("yinyu say:");
    9. System.out.println(chinese.getClass());
    10. }
    11. }

    Ⅳ 输出结果

    此时 application.properties 文件中依然配置 proxy-target-class = false~

    可以看到类被 CGLIB 增强了,也就是 CGLIB 动态代理!!说明如果目标对象没有实现接口的话,那么就会默认使用 CGLIB 动态代理。

    ③ 基于 xml 配置(以 JDK 动态代理为例)

    当然,SpringAOP 也是可以基于 xml 实现的,此时 xml 的作用相当于 Advice类文件(定义切面),同时也可将指定类注入 Spring。

    注意:以 JDK 动态代理为例,同时我注释了 proxy-target-class 的配置 👇

    Ⅰ 定义一个 XmlChinese 接口 :

    1. package com.aopdemo.aaxmlJdkDP;
    2. public interface XmlChinese {
    3. //定义一个 sayHello 的抽象方法
    4. void sayHello(String name);
    5. }

    Ⅱ 定义实现类–XmlYinyu:

    XmlYinyu 类实现 XmlChinese 接口,并重写 sayHello 这个抽象方法,同时不用注释做注入操作

    1. package com.aopdemo.aaxmlJdkDP;
    2. public class XmlYinyu implements XmlChinese {
    3. @Override
    4. public void sayHello(String name) {
    5. System.out.println(name + "hello, AOP");
    6. }
    7. }

    Ⅲ 编写 bean.xml 文件

    1. <beans xmlns="http://www.springframework.org/schema/beans"
    2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xmlns:context="http://www.springframework.org/schema/context"
    4. xmlns:aop="http://www.springframework.org/schema/aop"
    5. xsi:schemaLocation="
    6. http://www.springframework.org/schema/context
    7. http://www.springframework.org/schema/context/spring-context.xsd
    8. http://www.springframework.org/schema/aop
    9. http://www.springframework.org/schema/aop/spring-aop.xsd
    10. http://www.springframework.org/schema/beans
    11. http://www.springframework.org/schema/beans/spring-beans.xsd">
    12. <bean id="xmlYinyu" class="com.aopdemo.aaxmlJdkDP.XmlYinyu"/>
    13. <bean id="xmlEnpower" class="com.aopdemo.aaxmlJdkDP.XmlEnpower"/>
    14. <aop:config>
    15. <aop:aspect ref="xmlEnpower">
    16. <aop:before method="printbefore"
    17. pointcut="execution(public void com.aopdemo.aaxmlJdkDP.XmlYinyu.sayHello(..))"/>
    18. aop:aspect>
    19. aop:config>
    20. beans>

    Ⅳ 使用测试类进行测试

    1. package com.aopdemo;
    2. @SpringBootTest
    3. class AopdemoTests {
    4. @Test
    5. void xmlJdkDPTest() {
    6. ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
    7. XmlChinese xmlChinese = (XmlChinese) applicationContext.getBean("xmlYinyu");
    8. xmlChinese.sayHello("yinyu say to xml:");
    9. System.out.println(xmlChinese.getClass());
    10. }
    11. }

    Ⅴ 输出结果

    可以看到类型是 class com.sun.proxy.$Proxy108,显而易见这就是 JDK 的动态代理,因此使用 xml 进行动态代理时,默认 proxy-target-class = false 

    2 通过 AOP 打印数据访问层摘要(以 mybatis-plus 的 mapper 为例)

    2.1 项目初始化

    前提是项目已创建,可通过 IDEA Spring Initializr,且已导入web、mysql、lombok等依赖。

    ① 导入 mybatis-plus 依赖

    路径:pom.xml

    1. <dependency>
    2. <groupId>com.baomidougroupId>
    3. <artifactId>mybatis-plus-boot-starterartifactId>
    4. <version>3.5.2version>
    5. dependency>

    ② 连接数据库配置

    前提是连接的数据库存在与实体类相对应的数据表,数据表初始搭建详解在Spring MVC 实践详解文章的【2/2.1Mysql 数据库初始化】小节

    路径:src/main/resources/application.properties

    1. #数据库连接配置
    2. spring.datasource.username=root
    3. spring.datasource.password=root
    4. #mysql5~8 驱动不同driver-class-name 8需要增加时区的配置serverTimezone=UTC,放在url最后
    5. #useSSL=false 安全连接
    6. spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    7. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

    ③ 编写实体类

    路径:src/main/java/com/aopdemo/pojo/User.java

    1. package com.aopdemo.pojo;
    2. @Builder
    3. @Data
    4. @AllArgsConstructor
    5. @NoArgsConstructor
    6. public class User {
    7. @TableId(type = IdType.AUTO)//新增记录时未命名id时id自增
    8. private Long id;
    9. private String name;
    10. private Integer age;
    11. private String email;
    12. }

    2.2 AOP 具体代码

    ① 编写 mapper 接口

    路径:src/main/java/com/aopdemo/mapper/UserMapper.java

    1. package com.aopdemo.mapper;
    2. //在对应的接口上面继承一个基本的接口 BaseMapper
    3. @Mapper
    4. public interface UserMapper extends BaseMapper {
    5. //mybatisplus 将所有CRUD操作都编写完成了,不用像以前一样配置一大堆文件
    6. }

    ② 编写 service 实现类

    通过 mapper 定义了一些增删改查操作~

    1. package com.aopdemo.service;
    2. @Service
    3. public class UserServiceImpl {
    4. @Autowired
    5. private UserMapper userMapper;
    6. /*
    7. Mapper CRUD(增删改查)
    8. */
    9. //增加一个User
    10. public int addUser_ByMapper(User user){
    11. return userMapper.insert(user);
    12. }
    13. //根据id删除一个User
    14. public int deleteUserById_ByMapper(int id){
    15. return userMapper.deleteById(id);
    16. }
    17. //更新User
    18. public boolean updateUser_ByMapper(User user){
    19. userMapper.updateById(user);
    20. return true;
    21. }
    22. //根据id查询,返回一个User
    23. public User queryUser_ByMapper(int id){
    24. return userMapper.selectById(id);
    25. }
    26. //查询全部User,返回list集合
    27. public List queryAllUser_ByMapper(){
    28. return userMapper.selectList(new QueryWrapper<>());//QueryWrapper没有任何条件
    29. }
    30. }

    ③ 编写 Aspect 类

    针对 mapper 接口进行切入,该接口下的所有抽象方法都将得到增强!

    路径:src/main/java/com/aopdemo/aspect/PerformanceAspect.java

    1. package com.aopdemo.aspect;
    2. @Aspect
    3. @Component
    4. @Slf4j
    5. public class PerformanceAspect {
    6. //针对某个接口进行切面注入,ProceedingJoinPoint只是给 @Around 用的
    7. @Around(value = "execution(* com.baomidou.mybatisplus.core.mapper..*(..))")
    8. public Object logPerformance(ProceedingJoinPoint pjp) throws Throwable {
    9. long startTime = System.currentTimeMillis();
    10. String name = "--";
    11. String result = "YES";
    12. try {
    13. name = pjp.getSignature().toShortString();
    14. return pjp.proceed();
    15. } catch (Throwable t) {
    16. result = "NO";
    17. throw t;
    18. } finally {
    19. long endTime = System.currentTimeMillis();
    20. log.info("{};{};{}ms", name, result, endTime - startTime);
    21. }
    22. }
    23. }

    ④ 编写测试类

    路径:src/test/java/com/aopdemo/AopdemoTests.java

    1. package com.aopdemo;
    2. @Slf4j
    3. @SpringBootTest
    4. class AopdemoTests {
    5. @Autowired
    6. private UserServiceImpl userService;
    7. @Test
    8. void contextLoads() {
    9. log.info("All Users: {}", userService.queryAllUser_ByMapper());
    10. User user =userService.queryUser_ByMapper(1);
    11. log.info("The User of id = 1 : {}", user);
    12. }
    13. }

    ⑤ 输出结果

    说明 mapper 接口已被增强成功,按照切面增加的功能输出~


    参考文章

    通过 AOP 打印数据访问层摘要_L# S@的博客-CSDN博客

    5.Spring Aop xml配置代理 注解代理_多加香菜_0505的博客-CSDN博客

  • 相关阅读:
    计算机视觉领域经典模型汇总(2023.09.08
    【设计模式】适配器模式
    用vite搭建一个vue3 + ts项目
    vue项目读取全局配置
    python实战故障诊断之CWRU数据集(五):线性判别模型及二次判别模型的应用
    文件上传与Phar反序列化的摩擦
    STM32内部flash详解(1)
    AI大模型量化格式介绍(GPTQ,GGML,GGUF,FP16/INT8/INT4)
    VFP技巧
    学习总结 | 真实记录 MindSpore 两日集训营能带给你什么(一)!
  • 原文地址:https://blog.csdn.net/weixin_51407397/article/details/128088644