做网站用哪个服务器不用备案,网络技术学什么,外贸公司没网站,网站推广策划相信各位读者对于Spring AOP的理解都是一知半解#xff0c;只懂使用#xff0c;却不懂原理。网上关于Spring AOP的讲解层出不穷#xff0c;但是易于理解#xff0c;让人真正掌握原理的文章屈指可数。笔者针对这一痛点需求#xff0c;决定写一篇关于Spring AOP原理的优质博…相信各位读者对于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动态代理的开发步骤
创建原始对象目标对象定义额外功能附加功能定义切点组装切面 创建原始对象目标对象 定义额外功能附加功能
Spring提供了MethodBeforeAdvice接口这个接口中有一个before方法需要我们去实现。我们把额外功能书写在接口的实现中它会在原始方法执行之前运行额外功能。
定义切入点
切入点的定义额外功能加入的位置。
目的由程序员根据自己的需要决定将额外功能加入给哪个原始方法。比如register方法、login方法
如果将所有方法都作为切入点都加入额外的功能。那么可以写成如下
组装切面将第二步定义好的额外功能与第三步定义的切入点进行整合组装成切面
完美图解
那么我们该如何获得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方法中各个参数的具体含义
1Method method
它表示我们需要把这个额外功能添加到哪个原始方法中。如我们想要给register原始方法、login原始方法加入这个额外功能那么这里method就是register或者login。
2Object[] agrs
它表示额外功能所增加给的那个原始方法中所含有的形参。
比如参数method是login原始方法那么这里args对应的就是login方法中的参数。如下图login方法中的参数是String name和String password。因此这里args数组对应的就是这两个参数。
比如参数method是register原始方法那么这里args对应的就是register方法中的参数。如下图register方法中的参数是User user。因此这里args数组对应的就是这个参数。
由此可知args和method是息息相关的。
3Object 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。
2args
args代码示例
3within
within代码示例 4annotation
annotation的代码示例
切入点函数的逻辑运算
and与操作的代码示例
注意and操作不适用于同种类型的运算比如execution() and execution()是不行的。代码示例如下
or或操作符的代码示例
Spring AOP详解
AOPAspect Oriented Programing⾯向切⾯编程 Spring动态代理开发 以切⾯为基本单位的程序开发通过切⾯间的彼此协同相互调⽤完成程序的构建。切面 切入点 额外功能。OOP (Object Oritened Programing) ⾯向对象编程 Java 以对象为基本单位的程序开发通过对象间的彼此协同相互调⽤完成程序的构建。POP (Producer Oriented Programing) ⾯向过程⽅法、函数编程 比如C语言。以过程为基本单位的程序开发通过过程间的彼此协同相互调⽤完成程序的构建。
AOP本质就是Spring动态代理开发通过代理类为原始类增加额外功能。
好处利于原始类的维护。
注意AOP编程不可能取代OOPOOP编程有意补充。
AOP编程的开发步骤
原始对象额外功能MethodInterceptor切入点组装切面额外功能切入点
切面 切入点 额外功能
从几何角度来看面 点 相同的性质。
有两个核心问题
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动态代理中的classloaderEnhancer.setSuperClass()等效于JDK动态代理中的interfacesEnhancer.setCallback()等效于JDK动态代理中的invocationhandler。不过使用的是cglib中的MethodInterceptor而不是Spring提供的MethodInterceptor。
代码示例
JDK动态代理和CGlib动态代理的总结
JDK动态代理Proxy.newProxyInstance()通过接⼝创建代理的实现类Cglib动态代理 Enhancer 通过继承⽗类创建的代理类
现在我们来思考一下为什么Spring通过原始对象的id值获得的是代理对象呢
要弄明白这个细节需要把BeanPostProcessor和JDK动态代理/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。
代码示例
切入点复用
在切面类中定义一个方法上面有Pointcut注解。通过这种方式定义切点表达式后续更加有利于切入点复用。
如下图出现了代码冗余
解决切入点复用如下图
基于注解的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总结