全网最强剖析Spring AOP底层原理

news2024/11/15 22:15:35

相信各位读者对于Spring AOP的理解都是一知半解,只懂使用,却不懂原理。网上关于Spring AOP的讲解层出不穷,但是易于理解,让人真正掌握原理的文章屈指可数。笔者针对这一痛点需求,决定写一篇关于Spring AOP原理的优质博客。这篇文章深入浅出,层次递进,循序渐进讲解Spring AOP的底层原理。全文通俗易懂,图文并茂,原理和源码相结合,让你做到知其然知其所以然。通篇阅读预计半小时,相信你读完后,对Spring AOP会有全新的理解,有质的飞跃,面试时也能得心应手,回答游刃有余。

欢迎关注+点赞+收藏~

静态代理

Service层中包含了哪些代码?

Service层中 = 核心功能(几十行、上百代码)+ 额外功能(附加功能)

  • 核心功能:业务运算、Dao调用等
  • 额外功能:不属于业务、可有可无、代码量很小。比如事务、日志、性能等

举个现实生活中的例子:对于房东这个实体来说,他出租房屋主要包括广告看房和签合同收钱。但是他觉得自己一个人经常发广告看房太累了,只想签合同收钱。也就是对他来说发广告看房是额外功能,签合同收钱才是核心功能。

如果没有中介(代理人):
在这里插入图片描述

如果有中介(代理人):
在这里插入图片描述

这其实就是现实生活中,代理模式的影子。

那么什么是代理模式呢?

概念:通过代理类,为原始类(目标类)增加额外的功能。

使用代理设计模式的好处:利于原始类(目标类)的维护。

  • 目标类(原始类):指的是业务类(核心功能 --> 业务运算 DAO调⽤)
  • 目标方法(原始方法):目标类(原始类)中的方法
  • 额外功能(附加功能):日志、事务、性能

代理模式的核心:

代理类 = 原始类(目标类)+ 额外功能 + 原始类(目标类)实现相同的接口

也就是说在实现代理模式时,我们需要重点关注三点:原始类额外功能接口。其中原始类中执行的肯定是核心功能。额外功能其实是定义在代理类中的,在代理类中可以添加一些额外功能,并且在代理类中会引入原始对象(即原始类的对象),当代理类执行完额外功能后,就会通过原始对象去调用原始类中的核心功能。接口就是定义了一组行为规范,原始类和代理类都必须实现相同的接口。

下面是静态代理模式的一个简单代码示例:
在这里插入图片描述

  • 接口:UserSerivce。在这个接口中,我们会定义两个方法,即register方法和login方法。
  • 原始类:UserServiceImpl。这个原始类会实现UserSerivce接口中定义的方法,然后在这些方法中实现核心功能。
  • 额外功能:比如图片中的输出语句。UserServiceIProxy是静态代理类,它和原始类UserServiceImpl都要实现相同的UserSerivce接口中定义的方法。我们可以看到在UserServiceIProxy静态代理类中引入了原始类UserServiceImpl的原始对象userService。当在静态代理类中做完额外功能后,就会通过这个原始对象userService去调用原始类UserServiceImpl中的核心功能。

从上面我们可以知道,静态代理最大的特点:有一个原始类,就必须要有一个对应的静态代理类(比如原始类UserServiceImpl和对应的代理类UserServiceProxy;原始类OrderServiceImpl和对应的代理类OrderServiceProxy)。这些代理类都要由程序员手动写出来。为每⼀个原始类,手工编写⼀个代理类 (.java .class)。

可以总结出静态代理的缺点:

  • 静态类文件数量过多,不利于项目管理;
  • 额外功能维护性差,静态代理类中额外功能修改复杂;

由此引出下文的动态代理。

Spring动态代理入门

Spring动态代理的开发步骤:

  • 创建原始对象(目标对象)
  • 定义额外功能(附加功能)
  • 定义切点
  • 组装切面
  1. 创建原始对象(目标对象)
    在这里插入图片描述

  2. 定义额外功能(附加功能)

Spring提供了MethodBeforeAdvice接口,这个接口中有一个before方法需要我们去实现。我们把额外功能书写在接口的实现中,它会在原始方法执行之前运行额外功能。
在这里插入图片描述

  1. 定义切入点

切入点的定义:额外功能加入的位置。

目的:由程序员根据自己的需要,决定将额外功能加入给哪个原始方法。(比如register方法、login方法)

如果将所有方法都作为切入点,都加入额外的功能。那么可以写成如下:
在这里插入图片描述

  1. 组装切面(将第二步定义好的额外功能与第三步定义的切入点进行整合,组装成切面)
    在这里插入图片描述

完美图解:
在这里插入图片描述

那么我们该如何获得Spring工厂创建的动态代理对象,并进行调用呢?

正常来说,通过ctx.getBean("userService")得到的应该是UserServiceImpl这个类的对象。但在动态代理中,其实不是这样的。请注意,它实际上得到的是代理类的对象。

获取代理对象后,怎么确定这个代理对象的类型呢?

在静态代理中,我们知道代理类和原始类都要实现相同接口。在动态代理中其实也是一样的!我们可以通过原始类看它实现什么接口,那么这个代理对象的类型就是该接口。比如这里原始类UserServiceImpl实现UserService接口,那么这个代理对象的类型就是UserService
在这里插入图片描述

我们来实践出真知:
在这里插入图片描述

可以发现这和我们的结论确实是一致的!

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

在上述的动态代理开发中,我们并没有编写任何像静态代理中的UserServiceImplProxy代理类,根本就没有出现代理类的影子。那么Spring创建的动态代理类在哪里呢?

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

动态字节码技术:它并不需要.java.class,而是直接通过第三方的动态字节码框架,在JVM中创建对应类的字节码,进而创建对象,当虚拟机结束,动态字节码跟着消失。

动态代理的特点:动态代理不需要定义任何代理类文件,都是JVM运行过程中动态创建的,所以不会造成静态代理中类文件数量过多,影响项目管理的问题
在这里插入图片描述

动态代理编程简化代理的开发

在额外功能不改变的前提下,创建其他原始类的代理对象时,只需要指定原始对象即可。

比如我们要创建OrderServiceImpl这个原始类时,只需要在配置文件中指定原始对象即可:
在这里插入图片描述

动态代理额外功能的维护性大大增强

比如我们想要修改之前创建的额外功能before类,但其实我们并不需要在这个类中直接修改。而是可以新建一个额外功能before1类,在这个新类中完成添加额外功能操作。
在这里插入图片描述

Spring动态代理详解

从上面我们知道Spring动态代理开发四步骤:
在这里插入图片描述

下面我们将主要详细分析第二步额外功能和第三步切入点。
在这里插入图片描述

额外功能详解

分析before方法中各个参数的具体含义:
在这里插入图片描述

(1)Method method

它表示我们需要把这个额外功能添加到哪个原始方法中。如我们想要给register原始方法、login原始方法加入这个额外功能,那么这里method就是register或者login。

(2)Object[] agrs

它表示额外功能所增加给的那个原始方法中所含有的形参。

比如参数method是login原始方法,那么这里args对应的就是login方法中的参数。如下图,login方法中的参数是String name和String password。因此这里args数组对应的就是这两个参数。
在这里插入图片描述

比如参数method是register原始方法,那么这里args对应的就是register方法中的参数。如下图,register方法中的参数是User user。因此这里args数组对应的就是这个参数。
在这里插入图片描述

由此可知,args和method是息息相关的。

(3)Object target

它表示原始对象。比如原始类UserServiceImpl它的原始对象就是userService,原始类OrderServiceImpl它的原始对象就是orderService。

实践出真知:
在这里插入图片描述

MethodInterceptor(方法拦截器)

MethodBeforeAdvice和MethodInterceptor的区别:

  • MethodBeforeAdvice:额外功能只能运行在原始方法前。
  • MethodInterceptor:额外功能可以运行在原始方法之前或者原始方法之后或者原始方法前后。

我们编写一个类Around实现MethodInterceptor接口,这个接口中有一个invoke方法需要我们实现。这里方法中有一个形参MethodInvocation invocation。invoke方法的作用:额外功能书写在invoke中。
在这里插入图片描述

我们知道在MethodInterceptor中,额外功能可以运行在原始方法之前或者原始方法之后或者原始方法前后。那怎么确定这个额外功能到底要运行在原始方法之前、之后、前后呢?这个额外功能的运行时机我们怎么在invoke方法中体现出来呢?

在实现invoke方法过程中,我们必须要确定:原始方法怎么运行?

答:一旦我们确定原始方法怎么运行了,那么在原始方法之前写的额外功能就是运行在原始方法之前,在原始方法之后写的额外功能就是运行在原始方法之后,前后都写了就是运行在原始方法的前后。

我们需要先来掌握invoke这个方法的参数。

  • MethodInvocation invocation:它表示额外功能所要增加给的那个原始方法。如果我们所开发的这个额外功能是添加给login,那么invocation就代表login方法。

它有点类似于MethodBeforeAdvice中的Method method,不过它是对method的高级封装。

好了,现在我们已经知道invocation就代表原始方法。但是我们想要知道的是原始方法是怎么运行呢?
在这里插入图片描述

从源码中可以看出,其实invocation.proceed()就表示原始方法的运行。

  • 如果我们把额外功能添加给login原始方法,那么invocation.proceed()就代表login这个原始方法的运行;
  • 如果我们把额外功能添加给register原始方法,那么invocation.proceed()就代表register这个原始方法的运行。

我们再来看一下invoke方法的返回值:

返回值Object其实代表的是原始方法的返回值。也就是说,如果invocation代表login方法,那么Object其实就是login方法的返回值。

(1)额外功能运行在原始方法执行之前的代码示例
在这里插入图片描述

(2)额外功能运行在原始方法执行之后的代码示例
在这里插入图片描述

(3)额外功能运行在原始方法执行前后的代码示例
在这里插入图片描述

什么样的额外功能需要运行在原始方法执行的前后呢?

事务!如下图,原始方法执行之前开启事务,原始方法之前之后提交事务。
在这里插入图片描述

MethodInterceptor接口中invoke里面的额外功能也可以运行在原始方法抛出异常的时候:
在这里插入图片描述

总结MethodInterceptor中额外功能所运行的四个时机:

  • 额外功能运行在原始方法执行之前
  • 额外功能运行在原始方法执行之后
  • 额外功能运行在原始方法执行前后
  • 额外功能运行在原始方法抛出异常时

MethodInterceptor影响原始方法的返回值

如果原始方法的返回值直接作为invoke方法的返回值返回时,那么MethodInterceptor不会影响原始方法的返回值。
在这里插入图片描述

那么如果我们想要让MethodInterceptor影响原始方法的返回值呢?此时,我们不要在invoke中把原始方法的返回值作为invoke方法的返回值直接返回了。
在这里插入图片描述

切入点详解

在这里插入图片描述

切入点表达式分为三种:

  • 方法切入点表达式
  • 类切入点表达式
  • 包切入点表达式

(1)方法切入点表达式
在这里插入图片描述

定义login方法作为切入点:* login(..)
在这里插入图片描述

定义login方法且login方法有两个字符串类型的参数作为切入点:* login(String, String)
在这里插入图片描述

这里我们使用的参数类型是String,它是java.lang包中的类型。

注意:如果此时的参数类型是非java.lang包中的类型,那么必须要写全限定名。比如* register(com.baizhiedu.proxy.User)

注意:..可以和具体的参数类型连用。比如* login(String, ..)可以代表login(String)login(String,String)login(String,com.baizhiedu .proxy.User)
在这里插入图片描述

上面所讲解的这种切入点表达式并不精准!

如下图所示,如果使用* login(..),那么下图中的所有login方法都将可以匹配。
在这里插入图片描述

那么如何才能做到精准呢?

精准方法切入点限定:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(2)类切入点表达式
在这里插入图片描述

指定特定类作为切入点(额外功能加入的位置),自然这个类中的所有方法,都会加上对应的额外功能。
在这里插入图片描述

使用* *.UserServiceImpl.*(..),只能处理一层包。比如com是第一层包,然后UserServiceImpl类是在com这个包下,那么就是可以的。假设UserServiceImpl类是在com.baozhiedu.proxy包下,此时这是三层包,那么就不能正确切入。
在这里插入图片描述

那如果我非要把UserServiceImpl类是在com.baozhiedu.proxy包下呢?那么就应该写成:* *..UserServiceImpl.*(..),增加..其实就表示多级包。
在这里插入图片描述

(3)包切入点表达式
在这里插入图片描述

在这里插入图片描述

注意上面的这种写法* com.baizhiedu.proxy.*.*(..)必须要求UserServiceImpl类和OrderServiceImpl类都在proxy包中,而不能存在于proxy的子包中。

那么该如何处理上面的这个情况呢?可以写成* com.baizhiedu.proxy..*.*(..)
在这里插入图片描述

以上三种切入点表达式中,最具实战价值的是包切入点表达式

切入点函数

切入点函数的作用:用于执行切入点表达式。

切入点函数有:execution、args、within、@annotation。
在这里插入图片描述

(2)args

args代码示例:
在这里插入图片描述

(3)within
在这里插入图片描述

within代码示例:
在这里插入图片描述

在这里插入图片描述

(4)@annotation
在这里插入图片描述

@annotation的代码示例:
在这里插入图片描述

切入点函数的逻辑运算
在这里插入图片描述

and与操作的代码示例:
在这里插入图片描述

注意:and操作不适用于同种类型的运算,比如execution() and execution()是不行的。代码示例如下:
在这里插入图片描述

or或操作符的代码示例:
在这里插入图片描述

Spring AOP详解

  1. AOP(Aspect Oriented Programing):⾯向切⾯编程 = Spring动态代理开发 以切⾯为基本单位的程序开发,通过切⾯间的彼此协同,相互调⽤,完成程序的构建。切面 = 切入点 + 额外功能
  2. OOP (Object Oritened Programing) ⾯向对象编程 Java 以对象为基本单位的程序开发,通过对象间的彼此协同,相互调⽤,完成程序的构建。
  3. POP (Producer Oriented Programing) ⾯向过程(⽅法、函数)编程 比如C语言。以过程为基本单位的程序开发,通过过程间的彼此协同,相互调⽤,完成程序的构建。

AOP:本质就是Spring动态代理开发,通过代理类为原始类增加额外功能

好处:利于原始类的维护。

注意:AOP编程不可能取代OOP,OOP编程有意补充。

AOP编程的开发步骤

  1. 原始对象
  2. 额外功能(MethodInterceptor)
  3. 切入点
  4. 组装切面(额外功能+切入点)
  • 切面 = 切入点 + 额外功能

从几何角度来看,面 = 点 + 相同的性质
在这里插入图片描述

有两个核心问题:

  • AOP如何创建动态代理类(动态字节码技术)
  • Spring工厂如何加工创建代理对象(通过原始对象的id值,获得的是代理对象)

JDK动态代理

Proxy.newProxyInstance方法参数详解:
在这里插入图片描述

可以看到,newProxyInstance方法中含有三个参数:

  • ClassLoader loader它表示代理类的类加载器。传递给newProxyInstance的类加载器应该是已经被加载到内存中的类或接口的类加载器。这个类加载器将用来定义生成的代理类。
  • Class<?>[] interfaces它表示代理类所需要实现的接口列表。生成的代理类将实现这些接口中的每一个,使得代理实例可以被安全地转型为这些接口类型的任何一个。这些接口定义了代理实例可以调用哪些方法。
  • InvocationHandler h这是一个处理接口方法调用的调用处理器。当代理实例的方法被调用时,方法调用将被转发到这个调用处理器。这个处理器的invoke方法负责决定如何处理代理实例上的方法调用。这包括调用实际对象的方法、返回一个值或抛出一个异常等。这是实现动态代理功能的核心,允许开发者在运行时动态改变方法的行为。

在Java中,InvocationHandler 接口定义了一个非常重要的方法 invoke,它是实现动态代理的关键。当一个代理实例的方法被调用时,该方法就会被触发。InvocationHandler 的 invoke 方法有三个参数,每个参数都有其特定的用途和意义:
在这里插入图片描述

  • Object proxy这是代理类的实例本身,即调用方法的代理对象。通常不直接使用这个对象来调用方法,因为它可能会导致无限递归调用代理方法自身。这个参数主要用于反射相关的信息,比如可以通过它获取代理类的信息,但在方法内部调用它的其他方法时需要小心处理。
  • Method method额外功能所要增加给的那个原始方法。这是正被调用的方法的反射对象。这个 Method 对象包含了关于正在被调用的方法的所有元数据,如方法名、返回类型、参数类型等。通过这个对象,你可以访问到任何关于这个方法的信息,甚至可以通过反射来调用它。
  • Object[] args它表示原始方法中的形参。这是一个包含了传递给方法的所有参数的数组。这些是调用方法时实际使用的参数值。如果被调用的方法没有参数,则 args 将是一个长度为0的数组。通过这个数组,你可以修改、替换或调整传入方法的参数值,或者在调用原方法之前对它们进行处理。
    在这里插入图片描述

在这里插入图片描述

下面的这幅图中classloader借用的是类TestJDKProxy的classloader:
在这里插入图片描述

下面的这幅图中classloader借用的是UserService的classloader:
在这里插入图片描述

CGlib动态代理

CGlib创建动态代理的原理:父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样既可以保证二者方法⼀致,同时在代理类中提供新的实现(额外功能+原始方法)
在这里插入图片描述

如何通过cglib方法创建动态代理对象呢?

回顾一下JDK动态代理的过程,Proxy.newProxyInstance(classloader, interfaces, invocationhandler)。这个方法中需要用到三个参数。其实cglib也是同样的。只不过cglib中没有接口,因此不需要interfaces。但是cglib中是子类继承父类,因此需要保证继承关系。cglib需要三个参数:

  • Enhancer.setClassLoader():等效于JDK动态代理中的classloader
  • Enhancer.setSuperClass():等效于JDK动态代理中的interfaces
  • Enhancer.setCallback():等效于JDK动态代理中的invocationhandler。不过使用的是cglib中的MethodInterceptor而不是Spring提供的MethodInterceptor。

代码示例:
在这里插入图片描述

JDK动态代理和CGlib动态代理的总结:

  1. JDK动态代理Proxy.newProxyInstance()通过接⼝创建代理的实现类
  2. Cglib动态代理 Enhancer 通过继承⽗类创建的代理类

现在我们来思考一下"为什么Spring通过原始对象的id值,获得的是代理对象"呢?

要弄明白这个细节,需要把BeanPostProcessorJDK动态代理/CGlib动态代理结合起来分析。

下面这张图是创建代理过程中Spring通过BeanPostProcessor完成的对原始类UserServiceImpl的加工过程。
在这里插入图片描述

首先我们通过bean创建了原始类UserServiceImpl的原始对象,这里设置id是userService,因此userService就是UserServiceImpl原始类的对象。

Spring工厂第一步还是创建出了userService这个原始对象,它调用构造方法把原始对象创建好了。按照BeanPostProcessor加工时机来说,此时应该要进入postProcessBeforeInitialization对它进行加工。但是我们知道不一定都要进行postProcessBeforeInitialization和postProcessAfterInitialization这两次加工。因为我们很少做初始化操作。所以postProcessBeforeInitialization初始化之前的加工和postProcessAfterInitialization初始化后的加工实际上它们大概所起的作用是一样的。那么我们就可以不做postProcessBeforeInitialization这个加工处理了,在postProcessBeforeInitialization中直接return bean交还给Spring即可。总的来说就是创建完成userService这个原始对象后直接交给postProcessAfterInitialization这个方法来处理。

把创建好的原始对象userService传递给postProcessAfterInitialization方法的第一个参数Object bean。在postProcessAfterInitialization方法中的加工,要注意此时不像我们之前做的只是对属性进行简单的改变,我们要让userService这个原始对象通过我们的加工,最后把这个代理对象给创建出来。所以实际上从整个加工代码来讲,我们就用相应代理创建的底层代码来完成了。可以看到postProcessAfterInitialization方法中使用了JDK动态代理来创建出userServiceProxy代理对象。即通过Proxy.newProxyInstance()把userService原始对象加工成最终我们需要的userServiceProxy代理对象

这里关注一下Proxy.newProxyInstance()中的三个参数从何而来呢?

  • 对于classloader,我们随便借一个就行。
  • 对于interfaces,它是原始对象所实现的接口,这个原始就是userService,它传参给了bean,因此我们可以通过bean.getClass().getInterfaces()得到原始对象所实现的接口。
  • 对于invocationHandler,我们实现这个接口即可。

最后我们把加工后得到的代理对象userServiceProxy作为返回值交给Spring工厂。因此后续通过ctx.getBean("userService")得到的就是代理对象userServiceProxy而不是原始类UserServiceImpl它所对应的原始对象了。

代码示例:
在这里插入图片描述

Spring AOP编程

在这里插入图片描述

加入@Around()注解就等同于我们编写了一个类MyArround去实现了MethodInterceptor接口。然后我们声明的arround()方法(这个方法可以随便命名)就等同于里面的invoke方法。arround()方法的返回值就等同于invoke方法的返回值。arround()方法中的joinpoint就等同于invoke方法的invocation。

代码示例:
在这里插入图片描述

  1. 切入点复用

在切面类中定义一个方法,上面有@Pointcut注解。通过这种方式,定义切点表达式,后续更加有利于切入点复用。

如下图,出现了代码冗余:
在这里插入图片描述

解决切入点复用,如下图:
在这里插入图片描述

  1. 基于注解的AOP编程中动态代理的创建方式
    在这里插入图片描述

回顾AOP底层实现中的两种代理:

  • JDK 通过实现接口 做新的实现类方式 创建代理对象
  • Cglib通过继承父类 做新的子类 创建代理对象

那么基于注解的AOP编程中动态代理采用的是JDK还是Cglib呢?
在这里插入图片描述

从上图中可以看到,默认情况下AOP编程(包括传统的AOP开发和基于注解的AOP开发)底层采用的是JDK动态代理创建方式。

那么如果我们想要采用Cglib呢?

如下图,针对基于注解的AOP编程,我们可以在aop:aspectj-autoproxy中将proxy-target-class设置为true,那么就可以将默认的JDK动态代理修改为Cglib动态代理了。
在这里插入图片描述

如下图,针对传统的AOP开发,我们可以在aop:config标签中将proxy-target-class设置为true。
在这里插入图片描述

Spring AOP开发中的坑点

坑点:在同一个业务中,进行业务方法间的相互调用,只有最外层的方法,才是加入额外功能(内部的方法,通过普通的方式调用,都调用的是原始方法)。如果想让内层的方法也调用代理对象的方法,就要ApplicationContextAware获得工厂,进而获得代理对象。

先来看下图,给UserServiceImpl类中的所有方法(这里是register方法和login方法)都添加额外功能:
在这里插入图片描述

如下图,我们在register方法中调用了login方法,然后把测试类中的userService.login方法注释掉。从输出结果中可以看出,只给register方法添加了额外功能,并没有给login方法添加额外功能。这显然不符合我们的预期,按理来说应该是都要给register方法和login方法都添加上额外功能呀。为什么login方法没有执行额外功能呢?
在这里插入图片描述

我们重新来审视下面这个图,当测试类TestAspectProxt调用register方法时,我们是通过代理对象调的register方法(我们之前反复强调过ctx.getBean(“userService”)得到的是代理对象而并不是原始类UserServiceImpl它的原始对象)。然后我们在register方法中使用this调用了login方法,那么来思考一下这个login方法是属于哪个对象的?

从下图中显然看出login方法是属于this,而这里的this是指的类UserServiceImpl。我们知道UserServiceImpl是原始类,通过this.login它本质上是通过原始对象调的login方法。它并不是通过代理对象调的login。既然不是通过代理对象调的,那么显然你就不能拥有代理对象中添加的额外功能。因此这里只是普通的通过this调用本类UserServiceImpl中的login方法,没有涉及任何额外功能,那么输出结果肯定只有login方法中的输出内容,不会有代理对象中的额外功能。
在这里插入图片描述

明白了错因,如果想要解决问题,那么就要在register方法中拿到代理对象,进而通过代理对象调用login方法。最简单的办法就是如下图,把测试类中的这两行代码也写到register方法中:
在这里插入图片描述

但是我们知道Spring工厂是重量级资源,创建多个工厂ctx势必会占用大内存,一个应用中应该只能创建一个工厂。因此上述方法不推荐。

其实我们只需要实现ApplicationContextAware这个接口。由于在测试类中已经创建出了一个Spring工厂ctx,我们在UserServiceImpl类中定义一个ctx,然后实现ApplicationContextAware接口中的setApplicationContext方法,把测试类中创建好的那个Spring工厂直接赋值给UserServiceImpl类中的ctx。这样整体上就只有一个Spring工厂。

解决方法的代码示例:
在这里插入图片描述

Spring AOP总结

在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1859420.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

视频监控解决方案:视频平台升级技术方案(下)

目录 1 项目概况 2 项目需求 2.1 视频感知资源扩充 2.2 视频支撑能力升级 2.3 视频应用能力升级 3 技术设计方案 3.1系统总体架构 3.2视频感知资源扩充设计 3.3 视频支撑能力升级设计 3.4 视频应用能力升级设计 3.4.1视频资源目录管理 3.4.2标签管理 3.4.3设备智能…

万亿国债野外图传——天通卫星图传设备类目推荐

在远离都市喧嚣的辽阔自然中&#xff0c;户外工业作业以其独特的重要性日益凸显&#xff0c;涵盖了从高山峻岭的地质勘探、森林资源调查到广袤草原的生态监测等众多领域。然而传统监测方法不能全面覆盖&#xff0c;冰雪覆盖的山区和偏远地区的电力设施状况以及野生动物等户外状…

多功能推拉力测试机可实现焊球推力测试

LB-8100A 多功能推拉力测试机广泛应于与 LED 封装测试、IC 半导体封 装测试、TO 封装测试、IGBT 功率模块封装测试、光电子元器件封装测试、汽 车领域、航天航空领域、军工产品测试、研究机构的测试及各类院校的测试 研究等应用。 多功能推拉力测试机设置主要结构&#xff1a;…

DDD(data display debugger)调试工具

文章目录 DDD安装界面说明 DDD data display debugger是命令行调试程序&#xff0c;可以理解为可视化的GDB。 安装 CentOS下使用以下命令进行安装&#xff1a; yum install ddd等待安装完成即可。 界面说明 顺便写一个测试程序&#xff0c;编译可执行文件 终端命令行输入…

51单片机STC89C52RC——7.1 串口通信

目的/效果 实现单片机串口与电脑串口工具进行数据通讯&#xff0c; 1&#xff1a;设备向电脑串口发送HEX 2&#xff1a;让电脑串口工具控制单片机LED亮灭。同时让单片机反馈控制的结果。 一&#xff0c;STC单片机模块 二&#xff0c;串口通讯 2.1 串行通信与并行通信 &…

智心顾问:为心智障碍家庭带来温暖与专业支持

&#x1f499;关爱从心开始 —— 理解心智障碍 在这个世界上&#xff0c;有这样一群特殊的群体——心智障碍者。他们通常伴随着个体认知、社会互动和学习能力的障碍。这些障碍可能源于遗传、环境或未知因素&#xff0c;但不应成为他们照护者获得信息和支持的阻碍。心智障碍者的…

webStorm debug vue项目的两种方法

一、前言 本文将介绍通过webstorm对vue项目进行debugger调试的三种方案。 但是&#xff0c;不管通过那种方案&#xff0c;都无法达到类似后端idea调试的体验&#xff0c;感觉十分难受&#xff0c;不过&#xff0c;比起用console.log还是好一些。如果各位有更好的方案&#xf…

【本地知识库】本地知识库+语言大模型=知域问答

本地知识库语言大模型知域问答 本项目实质为本地知识库构建及应用&#xff0c;内容包含&#xff1a; 本地知识库构建及应用相关知识的介绍离线式本地知识库构建及应用在线式本地知识库构建及应用 本地知识库构建及应用相关知识的介绍 本地知识库 本地知识库通常是指存储在…

免费悬浮翻译器都有哪些?看剧学习都适配的翻译器分享~

不知道大家有没有设想过&#xff0c;当我们在查阅外文文档或是观看外语电影时&#xff0c;要是能够借助翻译工具来同步获取译文&#xff0c;那得有多快乐~ 不瞒大家说&#xff0c;现在想要实现这种快乐其实很简单&#xff01;只要手里头备好几个趁手的免费悬浮翻译器就可以搞定…

为冲刺IPO,喜马拉雅曝裁员20%?钉钉叶军吐槽百度搜索;美国制裁俄罗斯安全软件12名高管;华为自研语言仓颉力战Java

一、商业圈 1.钉钉总裁叶军吐槽百度搜索&#xff1a;前十条都是广告 钉钉总裁叶军在亚布力中国企业家论坛第十届创新年会上发表了演讲&#xff0c;期间他直言不讳地对百度搜索提出了批评。叶军指出&#xff0c;在OpenAI推出智能聊天机器人ChatGPT之后&#xff0c;百度的传统搜…

昇思25天学习打卡营第1天|基本介绍

今天的课程内容是MindSpore和昇腾AI全栈的整体介绍。 MindSpore 介绍 全场景深度学习框架 架构结构 扩展部分 Model Zoo 模型库Extend 扩展库&#xff08;强化学习/GNN&#xff09;Sciene 科学计算 开放部分MindExpression 统一API第三方前端 运行部分Data 数据处理AI编译…

k8s知识点

Kubernetes中Service、Ingress与Ingress Controller的作用与关系 Service 是对一组提供相同功能的 Pods 的抽象&#xff0c;并为它们提供一个统一的入口。借助 Service&#xff0c;应用可以方便的实现服务发现与负载均衡。Ingress 是反向代理规则&#xff0c;管理外部流量进入集…

环境安装-GIT

下载 git官网下载 https://git-scm.com/ 安装 点击下载的安装包&#xff0c;并点击下一步 选择安装路径&#xff0c;照例改选自定义路径 选择默认的即可 选择GIT编辑器&#xff0c;默认选择vim即可 设置初始化新项目(本地仓库)的主分支名&#xff0c;按默认即可&#xff0c;点…

web前端:作业四

1.编写一个函数&#xff0c;形参是一个数组&#xff0c;返回数组中所有数字的平均值 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><script>// 1.编写一个函数&#xff0…

基于Pytorch框架构建AlexNet模型

Pytorch 一、判断环境1.导入必要的库2.判断环境 二、定义字典1.定义字典 三、处理图像数据集1.导入必要的模块2.定义变量3.删除隐藏文件/文件夹 四、加载数据集1.加载训练数据集2.加载测试数据集3.定义训练数据集和测试集路径4.加载训练集和测试集5.创建训练集和测试集数据加载…

自定义User-Agent:使用Python Requests进行网络请求

在网络编程和数据采集领域&#xff0c;HTTP请求是与服务器交互的基本方式。User-Agent&#xff08;用户代理&#xff09;是HTTP请求中的一个重要字段&#xff0c;它告诉服务器发起请求的客户端类型和版本信息。在某些情况下&#xff0c;自定义User-Agent可以帮助我们模拟不同的…

Web 项目自动化测试主流框架都有哪些?

摘要&#xff1a;本文将详细介绍Web项目自动化测试的主流框架&#xff0c;包括Selenium、Cypress和Puppeteer。从零开始&#xff0c;我们将一步步引导您了解这些框架的使用和规范的书写。 引言&#xff1a;Web项目自动化测试是现代软件开发过程中不可或缺的一部分。它可以提高…

常微分方程算法之编程示例二(梯形法)

目录 一、研究问题 二、C代码 三、计算结果 一、研究问题 本节我们采用梯形法&#xff08;即隐式Eluer法&#xff09;求解算例。 梯形法的原理及推导请参考&#xff1a; 常微分方程算法之梯形法&#xff08;隐式Eluer法&#xff09;_梯形法求解常微分方程-CSDN博客https://…

Day12 单调栈 下一个最大元素

503. 下一个更大元素 II 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[0] &#xff09;&#xff0c;返回 nums 中每个元素的 下一个更大元素 。 数字 x 的 下一个更大的元素 是按数组遍历顺序&#xff0c;这个数字之后的第一个比它更大的数…

ES6+Vue

ES6Vue ES6语法 ​ VUE基于是ES6的&#xff0c;所以在使用Vue之前我们需要先了解一下ES6的语法。 1.什么是ECMAScript6 ECMAScript是浏览器脚本语言的规范&#xff0c;基于javascript来制定的。为什么会出现这个规范呢&#xff1f; 1.1.JS发展史 1995年&#xff0c;网景工…