• Spring AOP 详解


    个人博客地址:
    http://xiaohe-blog.top

    1. AOP概念

    AOP :面向切面编程,全称 Aspect Oriented Programing,简而言之,在不惊动原始设计的基础上为其进行功能增强。

    AOP本质就是Spring动态代理开发,通过代理类为原始类增加额外功能。我们知道,Spring动态代理就是将service层的附加业务抽取出来,让它专注核心业务的实现。

    AOP中的名词 :切面、连接点、切入点、通知。

    连接点 :JoinPoint,所有业务方法都是连接点。不管有没有添加额外功能。

    切入点 :PointCut,真正添加了额外功能的方法。

    通知 :Advice,额外功能(同样它也调用了核心业务)。

    切面 :Aspect,将切入点和通知结合。

    一定要分清连接点与切入点,业务方法都是连接点(可以连接的点)。但只有真正连接了额外方法才能叫作切入点。两者可以随时转变,我今天给一个业务增加额外功能,它叫切入点,明天我不给它加了,它就变回连接点了。

    现在我们要实现一个项目 :对用户的增删改,增删的同时记录此次操作所用时间,改的时候不需要记录用时。

    public interface UserService {
        public void add();
        public void delete();
        public void query();
    }
    
    public class UserServiceImpl implements UserService{
        @Autowired
        private UserMapper userMapper;
        public void add() {
            System.out.println("开始执行add方法");
            long start = System.currentTimeMillis();
            userMapper.addUser();
            long end = System.currentTimeMillis();
            long time = end - start;
            System.out.println("执行用时:" + time);
            System.out.println("add方法执行结束");
        }
        public void delete() {
            System.out.println("开始执行delete方法");
            userMapper.deleteUser();
            System.out.println("delete方法执行结束");
        }
        public void update() {
            System.out.println("开始执行update方法");
            userMapper.updateUser();
            System.out.println("update方法执行结束");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    现在add方法已经有记录时长的功能了,我们想要给delete加上这个功能,而update方法不需要这个功能。

    在此处,连接点是 add、delete、update。而切入点是没有update的,因为你此时不需要为update添加额外功能。

    image-20220826183111234

    那么我们如何使用AOP给这些切入点绑定通知呢?

    2. AOP开发

    AOP编程的开发步骤 :

    1. 连接点 :编写核心业务。
    2. 通知 :编写附加业务
    3. 组装切面 :挑选切入点,使用切面将切入点与通知连接。

    我们已经编写了连接点,只需要掌握通知与切面即可完成AOP。

    2.1 切入点

    我们已经确认了切入点是delete :

    public void delete() {
        System.out.println("开始执行delete方法");
        userMapper.deleteUser();
        System.out.println("delete方法执行结束");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2 通知

    AOP的通知有很多种 :

    • 前置通知 :通知先执行。
    • 后置通知 :通知后执行。
    • 环绕通知 :自己安排,想在哪就在哪。
    • 返回后通知
    • 异常后通知

    这几种通知用的最多的是 环绕通知,环绕通知中的核心业务可以在任意时刻执行,完成诸如核心业务前开启事务,核心业务后关闭事务这样的操作。

    为了防止战线拉的太长,笔者将通知与切入点表达式放到了文章末,尽量快点让大家接领会到什么是AOP

    要实现环绕通知,要先了解一个接口 :MethodInterceptor。

    image-20220825122806957

    它只有一个方法 :invoke,届时我们的额外功能都会在此处编写。

    MethodInvocation :核心业务,使用var1.proceed()可以调用核心业务。

    返回值Object :invoke()返回值与核心业务的返回值相同。但是我们怎么知道核心业务返回什么呢?var1.proceed()执行的是核心业务,那么这个方法的返回值不就是核心业务的返回值吗 ?

    所以我们可以将proceed的返回值返回 :

    @Override
    public Object invoke(MethodInvocation var1) throws Throwable {
        // 执行切入点的核心业务.
        Object proceed = var1.proceed();
        return procedd;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    于是我们的通知就可以这样写 :

    // 通知类
    public class MyAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation var1) throws Throwable {
            System.out.println("通知开始执行");
            long start = System.currentTimeMillis();
            Object object = var1.proceed();
            long end = System.currentTimeMillis();
            long time = end - start;
            System.out.println("执行用时:" + time);
            System.out.println("通知执行结束");
            return object;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.3 切面

    切面的目的是指定切入点并且绑定通知。这个过程可以使用注解形式完成,但是xml形式更加助于理解。

    在Spring配置文件中,使用标签实现切面。它有两个子标签,

    :用于指定切入点。

    :用于绑定切入点与通知。

    1. 注入通知。
    // 为什么将通知注入Spring?
    // 我们想要spring替我们执行额外功能,肯定要配置啊。
    <bean id="myAdvice" class="com.entity.MyAdvice"></bean>
    
    • 1
    • 2
    • 3
    1. 实现切面,
    <aop:config>
        
    	<aop:pointcut id="pc" 
        	expression="execution(public void com.entity.UserServiceImpl.delete())">
        aop:pointcut>	
        
        <aop:advisor advice-ref="myAdvice" pointcut-ref="pc">aop:advisor>
    aop:config>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    (是不是觉得很麻烦 ?这已经很简单了,看到那个expression属性了没?它叫做“切入点表达式”,下面要花很大功夫讲它。)

    这就是完整的aop开发步骤 :完成连接点、完成通知、编写切面。

    3. AOP原理分析

    上一节中使用getBean(“userService”)获得UserService的实现类还是UserServiceImpl吗?

    很明显不是了,我们可以获取它,执行delete,可以看到执行的已经不是原本的delete方法,因为在我们看不见的地方,Spring给我们组装好了动态代理对象。

    # 如果没有用到AOP,那么代码应该是这样的:
    UserService userService = new UserServiceImpl();
    # 用到了AOP,代码是这样的:
    UserService userService = new UserServiceProxy();
    
    • 1
    • 2
    • 3
    • 4
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = app.getBean("userService");
        userService.delete();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    image-20220826185604652

    image-20220827173316038

    那么Spring创建的动态代理类在哪里呢 ?

    Spring框架在运行时,通过动态字节码技术创建在JVM中,运行在JVM内部,等程序结束后,会和JVM一起消失。

    JVM运行一个类其实就是运行这个.java文件加载后的.class文件(字节码文件)。

    动态字节码技术不需要我们编写.java文件,它通过第三方框架直接动态生成代理对象的字节码文件。

    AOP的底层实现就是Spring动态代理的实现。通过动态代理创建出一个同时具有 额外功能 + 核心业务 的代理类

    如果你对动态代理技术不太了解,可以阅读这位大佬的博客 :http://t.csdn.cn/Z6x35

    那么问题来了,为什么我们通过getBean(“userService”)获得的Bean对象是动态代理对象而不是原对象呢?

    不仅是 getBean(“userService”),就连 getBean(UserServiceImpl.class) 都无法获取UserServiceImpl。

    肯定是 Spring 做的,它是如何实现的?

    在之前的学习中,我们接触到了 BeanPostProcessor 这个类,这个类在 Spring 创建 bean 对象之后执行。

    它有两个方法,一个before、一个after,before方法在 bean 调用构造函数之后&初始化之前执行,after在bean初始化之后执行。

    image-20220828193807403

    Spring在 BeanPostProcessor 中的 After 方法将UserServiceImpl类改为UserServiceProxy 并返回给我们,所以我们获取的不是实现类而代理类。

    image-20220828194659062

    4. 通知

    spring通知共5种 :

    1. 前置通知 Before :MethodBeforeAdvice
    2. 后置通知 After :AfterAdvice
    3. 环绕通知 Around :MethodInterceptor
    4. 返回后通知 After-returning :AfterReturningAdvice
    5. 异常后通知 After-throwing :ThrowsAdvice

    共有5中,常用的其实就一个环绕通知。但是前置通知和异常后通知需要讲一下。

    4.1 前置通知

    接口为 MethodBeforeAdvice

    image-20220826212845999

    Method :切入点,你想给delete增加额外方法,Method就是delete。

    Object[] :切入点的所有参数。delete的参数。

    Object :切入点所在类的实例。delete在UserService中,那Object就是UserService。

    下面动手完成AOP的前置通知 :

    AOP编程的步骤是一成不变的 :完成连接点、完成通知、编写切面。

    • 连接点:
    public void delete() {
        System.out.println("开始执行delete方法");
        userMapper.deleteUser();
        System.out.println("delete方法执行结束");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 前置通知
    public class MyBeforeAdvice implements MethodBeforeAdvice {
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("前置通知执行...");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 编写切面
    <bean id="myBeforeAdvice" class="com.entity.MyBeforeAdvice">bean>
    
    <aop:config>
    	<aop:pointcut id="pc" 
        	expression="execution(public void com.entity.UserServiceImpl.delete())">
        aop:pointcut>	
        <aop:advisor advice-ref="myBeforeAdvice" pointcut-ref="pc">aop:advisor>
    aop:config>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    以后的拦截器跟这个原理很像。

    4.2 异常后通知

    接口为 ThrowsAdvice。其中没有方法,需要我们自定义

    public class MyExceptionAdvice implements ThrowsAdvice {
    	public void afterThrowing(Exception ex) {
    		System.out.println("异常信息 :"+ex.getMessage());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    以后的全局异常处理器跟这个原理很像。

    5. 切入点表达式

    刚才咱们在切面中写的代码是 :

    expression="execution(public void com.entity.UserServiceImpl.delete())"
    
    • 1

    这个就是切入点表达式,试想一下,如果我们还有一个save、一个add需要添加这个通知,我们该怎么写?总不能再配置一个吧。这时就要学习切面表达式了。

    5.1 方法切入点

    方法切入点表达式由两部分组成 :修饰符+返回值、方法名(参数)。

    如果我们想要给所有方法加上通知,该怎么做?使用通配符 *

    因为分为两个部分,那么就是用两个 *,参数用两个 …代替,即为任意参数

    image-20220826224857756

    定义所有login作为切入点

    * login(..)
    
    • 1

    指定有两个String形参的所有login作为切入点

    * login(String, String)
    // 注意: 非java.lang包中的类型必须写全限定类名* login(com.xiaohe.User)
    
    • 1
    • 2
    • 3

    指定第一个参数为String,其他参数随意的所有login作为切入点

    * login(String, ..)
    
    • 1

    4.2 类切入点

    指定整个类为切入点,它的全部方法都会成为切入点。

    * com.service.UserServiceImpl.*(..)
    
    • 1

    4.3 包切入点

    指定整个包为切入点,这个包中的所有类的所有方法都成为切入点。

    * com.service.*.*()
    
    • 1

    太累了,写不下去了,种地去了。

  • 相关阅读:
    java实现多文件压缩
    护眼灯到底有用吗?保护孩子视力护眼灯推荐
    【web课程设计】用HTML+CSS做一个漂亮简单的动漫网页
    机器学习强基计划4-2:通俗理解极大似然估计和极大后验估计+实例分析
    记一次服务器Cuda驱动崩溃修复过程
    .NET Core反射获取带有自定义特性的类,通过依赖注入根据Attribute元数据信息调用对应的方法
    linux 中 mq_notify 创建线程监控消息队列实现原理
    如何使用Sentinel实现流控和降级
    【Java】springboot 枚举参数
    算法导论第一章——算法在计算中的应用
  • 原文地址:https://blog.csdn.net/qq_62939743/article/details/126573590