【Java开发】 Spring 07 :Spring AOP 实践详解(通过 AOP 打印数据访问层)
创始人
2024-03-13 23:35:16

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 依赖 👇

        org.aspectjaspectjweaver1.9.2

① JDK 动态代理

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

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

核心类包括 👇

  • InvocationHandler 接口
  • Proxy.newProxyInstance()

Ⅰ 定义一个 Chinese 接口 :

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

Ⅱ 实现类–Yinyu:

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

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

Ⅲ 定义Aspect

package com.aopdemo.ajdkDynamicProxy;@Aspect
@Component
public class DynamicProxyAdvice {//针对 Yinyu 类下的 sayHello 方法进行切入@Pointcut("execution(* com.aopdemo.ajdkDynamicProxy.Yinyu.sayHello(..))")public void pointcut() {}//接入点的前置操作@Before("pointcut()")public void before() {System.out.println("before");}
}

Ⅳ 使用测试类进行测试

package com.aopdemo;@SpringBootTest
class AopdemoTests {@Autowiredprivate Chinese chinese;@Testvoid ajdkDynamicProxyTest() {chinese.sayHello("yinyu say:");System.out.println(chinese.getClass());}
}

Ⅴ 配置 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

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

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

Ⅱ 定义Aspect

package com.aopdemo.aCGLIBDynamicProxy;@Aspect
@Component
public class CGLIBAdvice {//针对 CGLIBYinyu 类下的 eat 方法进行切入@Pointcut("execution(* com.aopdemo.aCGLIBDynamicProxy.CGLIBYinyu.eat(..))")public void pointcut() {}//接入点的前置操作@Before("pointcut()")public void before() {System.out.println("before");}}

Ⅲ 使用测试类进行测试

package com.aopdemo;@SpringBootTest
class AopdemoTests {@Autowiredprivate CGLIBYinyu cglibYinyu;@Testvoid ajdkDynamicProxyTest() {chinese.sayHello("yinyu say:");System.out.println(chinese.getClass());}
}

Ⅳ 输出结果

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

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

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

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

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

Ⅰ 定义一个 XmlChinese 接口 :

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

Ⅱ 定义实现类–XmlYinyu:

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

package com.aopdemo.aaxmlJdkDP;public class XmlYinyu implements XmlChinese {@Overridepublic void sayHello(String name) {System.out.println(name + "hello, AOP");}
}

Ⅲ 编写 bean.xml 文件


Ⅳ 使用测试类进行测试

package com.aopdemo;@SpringBootTest
class AopdemoTests {@Testvoid xmlJdkDPTest() {ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");XmlChinese xmlChinese = (XmlChinese) applicationContext.getBean("xmlYinyu");xmlChinese.sayHello("yinyu say to xml:");System.out.println(xmlChinese.getClass());}
}

Ⅴ 输出结果

可以看到类型是 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

        com.baomidoumybatis-plus-boot-starter3.5.2

② 连接数据库配置

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

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

③ 编写实体类

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

package com.aopdemo.pojo;@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {@TableId(type = IdType.AUTO)//新增记录时未命名id时id自增private Long id;private String name;private  Integer age;private  String email;
}

2.2 AOP 具体代码

① 编写 mapper 接口

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

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

② 编写 service 实现类

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

package com.aopdemo.service;@Service
public class UserServiceImpl {@Autowiredprivate UserMapper userMapper;/*Mapper CRUD(增删改查)*///增加一个Userpublic int addUser_ByMapper(User user){return userMapper.insert(user);}//根据id删除一个Userpublic int deleteUserById_ByMapper(int id){return userMapper.deleteById(id);}//更新Userpublic boolean updateUser_ByMapper(User user){userMapper.updateById(user);return true;}//根据id查询,返回一个Userpublic User queryUser_ByMapper(int id){return userMapper.selectById(id);}//查询全部User,返回list集合public List queryAllUser_ByMapper(){return userMapper.selectList(new QueryWrapper<>());//QueryWrapper没有任何条件}
}

③ 编写 Aspect 类

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

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

package com.aopdemo.aspect;@Aspect
@Component
@Slf4j
public class PerformanceAspect {//针对某个接口进行切面注入,ProceedingJoinPoint只是给 @Around 用的@Around(value = "execution(* com.baomidou.mybatisplus.core.mapper..*(..))")public Object logPerformance(ProceedingJoinPoint pjp) throws Throwable {long startTime = System.currentTimeMillis();String name = "--";String result = "YES";try {name = pjp.getSignature().toShortString();return pjp.proceed();} catch (Throwable t) {result = "NO";throw t;} finally {long endTime = System.currentTimeMillis();log.info("{};{};{}ms", name, result, endTime - startTime);}}
}

④ 编写测试类

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

package com.aopdemo;@Slf4j
@SpringBootTest
class AopdemoTests {@Autowiredprivate UserServiceImpl userService;@Testvoid contextLoads() {log.info("All Users: {}", userService.queryAllUser_ByMapper());User user =userService.queryUser_ByMapper(1);log.info("The User of id = 1 : {}", user);}
}

⑤ 输出结果

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


参考文章

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

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

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...