老生常谈系列之Aop--Spring Aop原理浅析
老生常谈系列之Aop--Spring Aop原理浅析
概述
上一篇介绍了AspectJ的编译时织入(Complier Time Weaver),其实AspectJ也支持Load Time Weaver, LTW依赖于java的agent,不了解的可以参考Oracle文档、JSR-163,现在市面上很多APM厂商监控Java就是基于agent。 通过替换c参数即可生效。由于本文主要方向为Spring Aop的原理,AspectJ的只是提一下,下文不再深入介绍。
// ${path}替换成你的路径
-javaagent:/${path}/aspectjweaver-1.8.3.jar
这篇会简单介绍Spring Aop的动态代理和例子,以及Spring Aop的简单实现原理,不同于Compiler Time Weaver和Loadtime Weaver,Spring Aop使用的是Runtime Weaver。
摘抄官网原文一段话。
Weaving: linking aspects with other application types or objects to create an advised object.
This can be done at compile time (using the AspectJ compiler, for example), load time, or at runtime.
Spring AOP, like other pure Java AOP frameworks, performs weaving at runtime.
考虑到Spring Aop的实现兼顾了很多细节,因此一开始这里不进行过多的具体实现讨论,以免一开始就陷入细节而无法自拔。为什么这篇叫原理浅析,就是不过分深入的研究各种实现细节,通过一些简单的例子和文档介绍,力争对Spring Aop的实现有个全局的观念。分析别人的代码实现,首要任务就是清晰方向,知晓原理才能在后面的深入分析不迷路。
既然明确了目标,那我这篇文章就结论先行,后续再去分析验证这个结论。这里直接抛出结论:Spring Aop使用的是动态代理,并且同时支持JDK动态代理和CGLIB代理。这里先简单介绍动态代理原理,然后推断Spring Aop的实现机制。
那么这里就涉及一个代理的概念,相信大家都懂。AOP的实现手段之一是建立在Java语言的反射机制与动态代理机制之上的。业务逻辑组件在运行过程中,AOP容器会动态创建一个代理对象供使用者调用,该代理对象已经按代码编写人员的意图将切面成功切入到目标方法的连接点上,从而使切面的功能与业务逻辑的功能都得以执行。从原理上讲,调用者直接调用的其实是AOP容器动态生成的代理对象,再由代理对象调用目标对象完成原始的业务逻辑处理,而代理对象则已经将切面与业务逻辑方法进行了合成。
动态代理分为JDK动态代理和CGLib动态代理,这两种Spring Aop都支持,这两种代理各有优劣。
JDK 动态代理用于对接口的代理,动态产生一个实现指定接口的类,注意动态代理有个约束:目标对象一定是要有接口的,没有接口就不能实现动态代理,只能为接口创建动态代理实例,而不能对类创建动态代理。
CGLIB 用于对类的代理,把被代理对象类的 class 文件加载进来,修改其字节码生成一个继承了被代理类的子类。使用 cglib 就是可以弥补动态代理的不足。
下面搞几个例子看一下用法。
JDK动态代理
代码例子
说到JDK动态代理,就不得不看java.lang.reflect
包下的一个接口InvocationHandler
,所有的代理类的InvocationHandler
实例都要实现这个接口,那这个接口的作用是什么?摘取一段接口上的注释如下:
{@code InvocationHandler} is the interface implemented by
the <i>invocation handler</i> of a proxy instance.
<p>Each proxy instance has an associated invocation handler.
When a method is invoked on a proxy instance, the method
invocation is encoded and dispatched to the {@code invoke}
method of its invocation handler.
翻译一下即为:code InvocationHandler
是代理实例的invocation handler
要实现的接口。 每个代理实例都有一个关联的调用处理程序。当在代理实例上调用方法时,方法调用被编码并分派到其调用处理程序的 invoke()
方法。
很显然,JDK动态代理的要实现这个接口,在代理实例上的方法调用都会被打包到这里的invoke()
方法来,这里就给我们提供了操作的空间,在真正执行调用method.invoke()
的前后,我们可以进行想要的操作。
Now,talk is cheap,show me your code.
首先自定义个MyInvocationHandler
类实现InvocationHandler
接口
/**
* @author Codegitz
* @date 2021/12/28 17:49
**/
public class MyInvocationHandler implements InvocationHandler {
private CalculateService calculateService;
public MyInvocationHandler(CalculateService calculateService){
this.calculateService = calculateService;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before invoke...");
return method.invoke(calculateService,args);
}
}
CalculateService
是我们需要代理的服务类,在invoke()
方法里面method.invoke(calculateService,args)
调用真正的业务逻辑,在前后插入逻辑实现想要的切面功能。
CalculateService
类代码。
/**
* @author Codegitz
* @date 2021/12/28 17:49
**/
public interface CalculateService {
public void calculate();
}
public class CalculateServiceImpl implements CalculateService {
@Override
public void calculate() {
System.out.println("calculate()");
}
}
有了service,有了InvocationHandler,接下来就是根据这两个根据生成一个代理,那么谁来生成代理类呢?接下来新建一个CalculateServiceProxy
生成代理类。
/**
* @author Codegitz
* @date 2021/12/28 17:53
**/
public class CalculateServiceProxy {
private CalculateService calculateService;
public CalculateServiceProxy(CalculateService calculateService){
this.calculateService = calculateService;
}
public Object getProxy(){
return Proxy.newProxyInstance(calculateService.getClass().getClassLoader(),calculateService.getClass().getInterfaces(),new MyInvocationHandler(new CalculateServiceImpl()));
}
}
以上就是一个简单的JDK动态代理的实现代码了,非常简单,三个要点业务Service,InvocationHandler,生成代理Proxy。
万事俱备只欠东风,写个测试跑一下。
/**
* @author Codegitz
* @date 2021/12/28 17:57
**/
public class ProxyTest {
@Test
public void proxyTest(){
CalculateService proxy = (CalculateService) new CalculateServiceProxy(new CalculateServiceImpl()).getProxy();
proxy.calculate();
}
}
结果符合预期,在调用真正的业务方法前,执行了我们自己的逻辑。以上就是简单的实现,CalculateService类是写死的,实际上这里应该写得更为通用,但这是代码样例,点到为止。
简单分析
功能是实现了,是不是有点好奇proxy.calculate()
中的proxy到底是什么,是通过什么方式实现了这个功能。下面直接把它的字节码扒拉下来,可以在测试方法的代码上加上这一句配置,但是这个只对JDK动态代理生效,CGLIB的不可通过此方式获取字节码。
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
JDK动态代理的类就会保存在项目的com.sun.proxy
目录下
然后用IDEA打开,可以看到反编译的代码如下:可以看到代理类重写了equals()
,toString()
,hashCode()
和calculate()
,前面三个都是Object自带的方法,暂时忽略,重点来看 calculate()
方法。
package com.sun.proxy;
import io.codegitz.service.CalculateService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy implements CalculateService {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
//...
}
public final String toString() throws {
//...
}
// 重点关注
public final void calculate() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
//...
}
// 静态代码块里获取对应的方法
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("io.codegitz.service.CalculateService").getMethod("calculate");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
可以看到super.h.invoke(this, m3, (Object[])null)
这句是最后执行了,super.h
就是我们自己实现的MyInvocationHandler
类,MyInvocationHandler#invoke()
方法会调用真正业务实现。
到这里,其实可以看出Proxy.newProxyInstance()
会把我们传进去的InvocationHandler实现类和proxy类关联起来,当proxy类执行方法时,最终都会经过InvocationHandler#invoke()
方法。这里雾里看花看了个大概,至于Proxy.newProxyInstance()
方法是怎么实现字节码修改并且返回一个代理类的,可以看JDK动态代理的实现细节。
你可以在测试代码加上这剩下的几个方法,执行结果是均会触发执行。
proxy.toString();
proxy.hashCode();
CGLIB代理
代码例子
上文介绍了JDK动态代理,那这里介绍另一个重要的代理CGLIB。CGLIB(Code Generation Library)是一个开源、高性能、高质量的Code生成类库(代码生成包)。
它可以在运行期扩展Java类与实现Java接口。Hibernate用它实现PO(Persistent Object 持久化对象)字节码的动态生成,Spring AOP用它提供方法的interception(拦截)。
CGLIB的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。但不鼓励大家直接使用ASM框架,因为对底层技术要求比较高。
使用CGLIB就绕不开Enhancer
类,查看一下类上注释
Generates dynamic subclasses to enable method interception. This
class started as a substitute for the standard Dynamic Proxy support
included with JDK 1.3, but one that allowed the proxies to extend a
concrete base class, in addition to implementing interfaces. The dynamically
generated subclasses override the non-final methods of the superclass and
have hooks which callback to user-defined interceptor
implementations.
翻译一下:这个类用以生成动态子类以启用方法拦截。 此类最初是作为 JDK 1.3 中包含的标准动态代理支持的替代品,但除了实现接口之外,它还允许代理扩展具体的基类。 动态生成的子类覆盖超类的非final方法,并具有回调到用户定义的拦截器实现的钩子。
根据注释的描述,用得最多的是MethodInterceptor
方法拦截器,下面的例子我们用MethodInterceptor
实现,下面来看看例子。
首先实现MethodInterceptor
拦截器
/**
* @author Codegitz
* @date 2021/12/29 10:59
**/
public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before invoke by cglib...");
return methodProxy.invokeSuper(object,objects);
}
}
业务类复用上面的CalculateService
,直接写测试方法。
@Test
public void cglibProxyTest(){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CalculateServiceImpl.class);
enhancer.setCallback(new MyMethodInterceptor());
CalculateService calculateService = (CalculateService) enhancer.create();
calculateService.calculate();
}
测试结果如下,符合预期,这就是CGLIB的简单用法,是不是简单的有一点难以置信。
简单分析
如法炮制,把CGLIB生成的字节码扒拉下来看看,在测试代码Enhancer
前添加
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"C:\\code");
可以看到对应的目录下生成了三个class文件,其中第二是我们想要的代理类的字节码。
反编译出来的码很长,这里只贴关注的部分出来。
public class CalculateServiceImpl$$EnhancerByCGLIB$$4f204217 extends CalculateServiceImpl implements Factory {
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
private static final Method CGLIB$calculate$0$Method;
private static final MethodProxy CGLIB$calculate$0$Proxy;
// ...
static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class var0 = Class.forName("io.codegitz.service.CalculateServiceImpl$$EnhancerByCGLIB$$4f204217");
Class var1;
Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
//...
CGLIB$calculate$0$Method = ReflectUtils.findMethods(new String[]{"calculate", "()V"}, (var1 = Class.forName("io.codegitz.service.CalculateServiceImpl")).getDeclaredMethods())[0];
CGLIB$calculate$0$Proxy = MethodProxy.create(var1, var0, "()V", "calculate", "CGLIB$calculate$0");
}
final void CGLIB$calculate$0() {
super.calculate();
}
// 代理后的 calculate()方法
public final void calculate() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$calculate$0$Method, CGLIB$emptyArgs, CGLIB$calculate$0$Proxy);
} else {
super.calculate();
}
}
// 省略部分代码...
}
CGLIB的原理不多赘述,不然要跑偏陷入细节了。我们管中窥豹,可以看到这里获取了MethodInterceptor
,那么这里显然就是我们实现的MyMethodInterceptor
拦截类。如果获取到的拦截器不为空,那么就会通过MyMethodInterceptor#intercept()
方法去调用,所以会执行到我们在拦截器里加入的逻辑。仔细一想,MethodInterceptor
是不是跟JDK动态代理的InvocationHandler
有异曲同工之妙,都是都过封装一次方法的调用,把原有的方法调用转发到我们自定义的invoke()
方法上,通过一次转发,提供了额外操作的空间。至于CGLIB是怎么生成字节码和字节码结构的详细解析,可以看这篇CGLIB动态代理的实现细节。
Spring Aop的原理浅析
前戏做足,到这里,才进入正题,这小节是后续文章的引子。前面花了比较大的篇幅去介绍了JDK动态代理和CGLIB代理,同时在文章的开头我们知道一个结论Spring Aop是使用动态代理,那么Spring Aop是怎么使用动态代理的呢?
先来看一下Spring Aop中注解实现的切面写法。我这里的例子使用的都是注解驱动,就不搞xml那一套了,原理都是一样的。
定义业务类
/**
* @author Codegitz
* @date 2021/12/29 17:55
**/
@Component
public class LoginService {
public void login(String name){
System.out.println("login..." + name);
}
}
定义切面
/**
* @author Codegitz
* @date 2021/12/29 17:53
**/
@Aspect
@Component
public class SpringAopAspect {
@Pointcut("execution(* io.codegitz.aop.demo.service.LoginService.login(..))")
public void login(){}
@Before("login()")
public void before(){
System.out.println("before login...");
}
}
开启Aop支持
/**
* @author Codegitz
* @date 2021/12/29 18:01
**/
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
写个Application跑一下
/**
* @author Codegitz
* @date 2021/12/29 18:05
**/
@ComponentScan(basePackages = "io.codegitz.aop.demo")
public class Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
LoginService loginService = (LoginService) applicationContext.getBean("loginService");
loginService.login("codegitz");
}
}
可以看到代理生效了,这个类的命名方式是不是很熟悉,这就是CGLIB生成的动态代理的命名。切面逻辑生效,输出符合预期。
可以看到,Spring最后生成的代理类跟我们上面直接使用CGLIB生成的是一样的。那么我们可以开始推理,这两个代码的起点是不一样的,Spring 的起点是一个@Aspect
注解的类,而我们的测试代码是直接实现一个MethodInterceptor
拦截器。由此是不是可以猜测Spring是把切面类的切面逻辑封装成了拦截器,然后在创建Bean的时候判断是否需要代理,然后再把拦截器注入生成动态代理替换原有的bean,从而实现了Aop功能?
目前猜测的逻辑如下:
这个图展示了我们猜测的Spring Aop使用CGLIB的代理逻辑,使用JDK动态代理逻辑应该也类似。那接下来,我们就从例子入手,另起文章,分析Spring Aop的源码实现。估计源码解析会分上下两篇文章去分析,正在酝酿。
总结
这篇文件叫浅析,实际上并没有去分析源码,而是通过推断概况给出了Spring Aop的原理,所以命名可能有点不妥,但是我不想改,就这样吧。这篇文章主要介绍了两种动态代理,并且简单写了例子和分析。然后我们写了一个Spring Aop的例子,实现了Aop的逻辑,但是碍于篇幅,我并没有在这里展开Spring Aop的实现,而是准备另起文章去分析。同时,我们另起的文章依赖于本文的一个推断:Spring是把切面类的切面逻辑封装成了拦截器,然后在创建Bean的时候进行了代理。可以牢牢记住这个推断,下文会去验证是否就是如此。
有时候抓住了目标,就不会迷路,可以在繁杂的逻辑中,理清脉络,因为我们知道最终要达到的效果,所以会知道这一步的目的是做什么,而不是茫无目的,到处乱逛。走马观花,终究不会深刻。接下来写实现分析,行百里者半九十,坚持就是胜利。
老生常谈系列之Aop--Spring Aop原理浅析的更多相关文章
- Spring系列22:Spring AOP 概念与快速入门篇
本文内容 Spring AOP含义和目标 AOP相关概念 声明式AOP快速入门 编程式创建代理对象 Spring AOP含义和目标 OOP: Object-oriented Programming 面 ...
- Spring系列25:Spring AOP 切点详解
本文内容 Spring 10种切点表达式详解 切点的组合使用 公共切点的定义 声明切点@Poincut @Poincut 的使用格式如下: @Poincut("PCD") // 切 ...
- Spring系列26:Spring AOP 通知与顺序详解
本文内容 如何声明通知 如何传递参数到通知方法中 多种通知多个切面的通知顺序 多个切面通知的顺序源码分析与图解 声明通知 Spring中有5种通知,通过对应的注解来声明: @BeforeBefore ...
- 【Spring】Spring系列3之Spring AOP
3.Spring AOP 3.1.AOP概述 3.2.前置通知 3.3.后置通知 3.4.返回通知.异常通知.环绕通知 3.5.指定切面优先级 3.6.重用切入点表达式 3.7.引入通知 3.8.基于 ...
- 框架源码系列十:Spring AOP(AOP的核心概念回顾、Spring中AOP的用法、Spring AOP 源码学习)
一.AOP的核心概念回顾 https://docs.spring.io/spring/docs/5.1.3.RELEASE/spring-framework-reference/core.html#a ...
- Spring系列(四):Spring AOP详解和实现方式(xml配置和注解配置)
参考文章:http://www.cnblogs.com/hongwz/p/5764917.html 一.什么是AOP AOP(Aspect Oriented Programming),即面向切面编程, ...
- Spring mvc 原理浅析
2.2. 数据的绑定 前面说过了,SpringMVC是方法级的映射,那么Spring是如何处理方法签名的,又是如何将表单数据绑定到方法参数中的?下面我们就来讨论这个问题.2.2.1. 处理方法签名 首 ...
- Spring Aop技术原理分析
本篇文章从Aop xml元素的解析开始,分析了Aop在Spring中所使用到的技术.包括Aop各元素在容器中的表示方式.Aop自动代理的技术.代理对象的生成及Aop拦截链的调用等等.将这些技术串联起来 ...
- Spring5.0源码学习系列之Spring AOP简述
前言介绍 附录:Spring源码学习专栏 在前面章节的学习中,我们对Spring框架的IOC实现源码有了一定的了解,接着本文继续学习Springframework一个核心的技术点AOP技术. 在学习S ...
- Spring 学习——Spring AOP——AOP概念篇
AOP AOP的定义:AOP,Aspect Oriented Programming的缩写,意为面向切面编程,是通过预编译或运行期动态代理实现程序功能处理的统一维护的一种技术 实现方式 预编译 Asp ...
随机推荐
- Vue手动集成less预编译器
less是一门css预处理语言,简单的说就是在css的基础上提升为可编程性的预编译器 需要在项目中安装 less ,less-loader 2个插件,语法为:npm i -D less less-lo ...
- 为什么使用 Executor 框架?
每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时. 耗资源的. 调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建, 线程之间的相互 ...
- 使用 Spring 通过什么方式访问 Hibernate?
在 Spring 中有两种方式访问 Hibernate:控制反转 Hibernate Template 和 Callback.继承 HibernateDAOSupport 提供一个 AOP 拦截器.
- nginx静态资源服务器配置
编辑 nginx.conf server { listen 80; server_name file.youxiu326.xin; location /image/ { #访问 file.youxiu ...
- 攻防世界shrine
shrine import flask import os app = flask.Flask(__name__) app.config['FLAG'] = os.environ.pop('FLAG' ...
- python学习笔记(八)——文件操作
在 windows 系统下,我们通过 路径+文件名+扩展名的方式唯一标识一个文件,而在 Linux 系统下通过 路径+文件名唯一标识一个文件. 文件分类:文件主要可以分为文本文件和二进制文件,常见的如 ...
- android webview与jquery mobile相互通信
最近做android项目中遇到要在webview中做与js交互相关的东东,涉及到js中调用android本地的方法,于是查了资料整理了一下android和js互相调用的过程.如下demo,demo的主 ...
- 【promise| async/await】代码的控制力
什么样的代码好控制? 结构 + 节奏 --- 什么鬼? 如何控制节奏? 具体例子看看怎么控制节奏?
- 【Android开发】分割字符串工具类
public class TextUtils { public static String[] results; /** * 分隔符:"." * * @param resource ...
- docker容器与虚拟机区别