Spring_AOP实现的三种方式

1、什么是AOP

假设我们接到了一个历史悠久的系统,一天你的上司告诉你这个系统的日志打印的不是很全,需要你多加一些日志打印,这时你还对系统不熟悉,不敢瞎碰代码的时候,AOP就可以帮你解决。

AOP(Aspect-Oriented Programming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需 要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日 志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种 散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为 “Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低 模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为; 那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手 将这些剖开的切面复原,不留痕迹。

这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。

使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。

从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。

这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了 将不同的关注点分离出来的效果。

2、AOP的相关概念

  • join point(连接点):是程序执行中的一个精确执行点,例如类中的一个方法。它是一个抽象的概念,在实现AOP时,并不需要去定义一个join point。
  • point cut(切入点):本质上是一个捕获连接点的结构。在AOP中,可以定义一个point cut,来捕获相关方法的调用。
  • advice(通知):是point cut的执行代码,是执行“方面”的具体逻辑。
  • aspect(方面):point cut和advice结合起来就是aspect,它类似于OOP中定义的一个类,但它代表的更多是对象间横向的关系。
  • introduce(引入):为对象引入附加的方法或属性,从而达到修改对象结构的目的。有的AOP工具又将其称为mixin。

3、实现原理

AOP分为静态AOP和动态AOP。

静态AOP是指AspectJ实现的AOP,他是将切面代码直接编译到Java类文件中。

动态AOP是指将切面代码进行动态织入实现的AOP。

Spring的AOP为动态AOP,实现的技术为: JDK提供的动态代理技术 和 CGLIB(动态字节码增强技术) 。尽管实现技术不一样,但 都是基于代理模式 , 都是生成一个代理对象 。

那么Spring默认使用的是JDK提供的动态代理呢,是CGLIB动态代理呢,我们看一下SpringAOP部分的源码:
1.png
这里我们不难看出,如果被代理对象实现了接口,那么就使用JDK的动态代理技术,反之则使用CGLIB来实现AOP,所以 Spring默认是使用JDK的动态代理技术实现AOP的 。

有人说Spring 5.x以后都是CGLIB,这里我贴一篇文章,这里写的很详细:

https://blog.csdn.net/lilizhou2008/article/details/102814250

4、代码实现

我们模拟一个用户的增删改查

UserService:

  1. package com.zym.service;
  2. public interface UserService {
  3. public void add();
  4. public void delete();
  5. public void update();
  6. public void query();
  7. }

UserServiceImpl

  1. package com.zym.service.impl;
  2. import com.zym.service.UserService;
  3. public class UserServiceImpl implements UserService {
  4. public void add() {
  5. System.out.println("添加方法");
  6. }
  7. public void delete() {
  8. System.out.println("删除方法");
  9. }
  10. public void update() {
  11. System.out.println("更新方法");
  12. }
  13. public void query() {
  14. System.out.println("查询方法");
  15. }
  16. }

然后我们需要在配置类中添加AOP的头文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:aop="http://www.springframework.org/schema/aop"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans
  6. https://www.springframework.org/schema/beans/spring-beans.xsd
  7. http://www.springframework.org/schema/aop
  8. http://www.springframework.org/schema/aop/spring-aop.xsd">

方法一:使用原生的Spring API

  1. <!--注册bean-->
  2. <bean id="userService" class="com.zym.service.impl.UserServiceImpl"/>
  3. <bean id="log" class="com.zym.log.Log"/>
  4. <bean id="afterlog" class="com.zym.log.AfterLog"/>
  5. <!-- 方式一:使用原生的Spring API 接口-->
  6. <!-- 配置AOP-->
  7. <aop:config>
  8. <!--切入点 expression:表达式,execution(要执行的位置)-->
  9. <aop:pointcut id="pointcut" expression="execution(* com.zym.service.impl.UserServiceImpl.*(..))"/>
  10. <!--执行环绕-->
  11. <aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
  12. <aop:advisor advice-ref="afterlog" pointcut-ref="pointcut"/>
  13. </aop:config>

Log.java

  1. package com.zym.log;
  2. import org.springframework.aop.MethodBeforeAdvice;
  3. import java.lang.reflect.Method;
  4. public class Log implements MethodBeforeAdvice {
  5. public void before(Method method, Object[] args, Object target) throws Throwable {
  6. System.out.println(target.getClass().getName()+"的"+method.getName()+"被执行了");
  7. }
  8. }

AfterLog.java

  1. package com.zym.log;
  2. import org.springframework.aop.AfterReturningAdvice;
  3. import java.lang.reflect.Method;
  4. public class AfterLog implements AfterReturningAdvice {
  5. public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
  6. System.out.println("执行了"+method.getName()+"方法,返回结果为:"+o);
  7. }
  8. }

测试类:

  1. package com.zym;
  2. import com.zym.service.UserService;
  3. import org.springframework.context.ApplicationContext;
  4. import org.springframework.context.support.ClassPathXmlApplicationContext;
  5. public class main {
  6. public static void main(String[] args) {
  7. ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  8. //动态代理 代理的是接口
  9. UserService userService = context.getBean("userService", UserService.class);
  10. userService.add();
  11. }
  12. }

结果:
2.png
这里重点说一下expression=”execution( com.zym.service.impl.UserServiceImpl.(..)) 这段配置的含义:

  1. 第一个*号:表示返回值类型,*号表示所有的类型。
  2. 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包
  3. 比如com.zym.service.impl.. 就代表impl包与它下面的所有子包
  4. 第二个*号:表示类名,*号表示所有的类。
  5. *(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。

然后我们回看配置,其实很简单,配置一个切点,再配置切点对应的方法,就可以实现横向切入了,这里的方法要先以Bean注入到Spring中

方法二、自定义类

想实现自定义类来实现AOP,需要先导入aspectj的依赖

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。

  1. <dependency>
  2. <groupId>org.aspectj</groupId>
  3. <artifactId>aspectjweaver</artifactId>
  4. <version>1.9.6</version>
  5. </dependency>

因为是自定义类实现AOP,我们新创建一个自定义的类:

DiyPointCut.java

  1. package com.zym.diy;
  2. public class DiyPointCut {
  3. public void before(){
  4. System.out.println("===========方法执行前=============");
  5. }
  6. public void after(){
  7. System.out.println("===========方法执行后=============");
  8. }
  9. }

此处定义了两个方法,执行前与执行后

配置文件修改:

  1. <!--方式二:自定义类-->
  2. <bean id="diy" class="com.zym.diy.DiyPointCut"/>
  3. <aop:config>
  4. <!--自定义切面,ref要引用的类-->
  5. <aop:aspect ref="diy">
  6. <!--切入点-->
  7. <aop:pointcut id="pointcut" expression="execution(* com.zym.service.impl.UserServiceImpl.*(..))"/>
  8. <!--通知-->
  9. <aop:before method="before" pointcut-ref="pointcut"/>
  10. <aop:after method="after" pointcut-ref="pointcut"/>
  11. </aop:aspect>
  12. </aop:config>

结果:
3.png
其实类似于方式一,比起方式一可以不用那么多Bean,只需要一个Bean即可!

方式三、注解:

注解只需要在配置文件中开启,然后把你的注解类作为Bean注入Spring

  1. <!--方式三:注解-->
  2. <!--开启注解支持-->
  3. <aop:aspectj-autoproxy/>
  4. <bean id="annotationPointCut" class="com.zym.diy.AnnotationPointCut"/>

AnnotationPointCut.java

  1. package com.zym.diy;
  2. import org.aspectj.lang.ProceedingJoinPoint;
  3. import org.aspectj.lang.annotation.After;
  4. import org.aspectj.lang.annotation.Around;
  5. import org.aspectj.lang.annotation.Aspect;
  6. import org.aspectj.lang.annotation.Before;
  7. //使用注解方式实现aop
  8. @Aspect //标注这个类是一个切面
  9. public class AnnotationPointCut {
  10. @Before("execution(* com.zym.service.impl.UserServiceImpl.*(..))")
  11. public void before(){
  12. System.out.println("***********方法执行前***********");
  13. }
  14. @After("execution(* com.zym.service.impl.UserServiceImpl.*(..))")
  15. public void after(){
  16. System.out.println("***********方法执行后***********");
  17. }
  18. //在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点
  19. @Around("execution(* com.zym.service.impl.UserServiceImpl.*(..))")
  20. public void around(ProceedingJoinPoint joinPoint) throws Throwable {
  21. System.out.println("环绕前");
  22. Object proceed = joinPoint.proceed();
  23. System.out.println("环绕后");
  24. }
  25. }

结果:
4.png
SpringAOP实现横向切入是不是很简单呢?当然,我们介绍了它在Spring中的用法,我们也说一下它的优势:

  • 在定义应用程序对某种服务(例如日志)的所有需求的时候。通过识别关注点,使得该服务能够被更好的定义,更好的被编写代码,并获得更多的功 能。这种方式还能够处理在代码涉及到多个功能的时候所出现的问题,例如改变某一个功能可能会影响到其它的功能,在AOP中把这样的麻烦称之为“纠结 (tangling)”。
  • 利用AOP技术对离散的方面进行的分析将有助于为开发团队指定一位精于该项工作的专家。负责这项工作的最佳人选将可以有效利用自己的相关技能和经验。
  • 持久性。标准的面向对象的项目开发中,不同的开发人员通常会为某项服务编写相同的代码,例如日志记录。随后他们会在自己的实施中分别对日志进 行处理以满足不同单个对象的需求。而通过创建一段单独的代码片段,AOP提供了解决这一问题的持久简单的方案,这一方案强调了未来功能的重用性和易维护 性:不需要在整个应用程序中一遍遍重新编写日志代码,AOP使得仅仅编写日志方面(logging aspect)成为可能,并且可以在这之上为整个应用程序提供新的功能。

总而言之,AOP技术的优势使得需要编写的代码量大大缩减,节省了时间,控制了开发成本。同时也使得开发人员可以集中关注于系统的核心商业逻辑。此外,它更利于创建松散耦合、可复用与可扩展的大型软件系统。

参考链接:https://www.cnblogs.com/jingzhishen/p/4980551.html