Spring AOP 源码分析 - 创建代理对象
1.简介
在上一篇文章中,我分析了 Spring 是如何为目标 bean 筛选合适的通知器的。现在通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 bean 的某些方法前后。与筛选合适的通知器相比,创建代理对象的过程则要简单不少,本文所分析的源码不过100行,相对比较简单。在接下里的章节中,我将会首先向大家介绍一些背景知识,然后再去分析源码。那下面,我们先来了解一下背景知识。
2.背景知识
2.1 proxy-target-class
在 Spring AOP 配置中,proxy-target-class 属性可影响 Spring 生成的代理对象的类型。以 XML 配置为例,可进行如下配置:
<aop:aspectj-autoproxy proxy-target-class="true"/>
<aop:config proxy-target-class="true">
<aop:aspect id="xxx" ref="xxxx">
<!-- 省略 -->
</aop:aspect>
</aop:config>
如上,默认情况下 proxy-target-class 属性为 false。当目标 bean 实现了接口时,Spring 会基于 JDK 动态代理为目标 bean 创建代理对象。若未实现任何接口,Spring 则会通过 CGLIB 创建代理。而当 proxy-target-class 属性设为 true 时,则会强制 Spring 通过 CGLIB 的方式创建代理对象,即使目标 bean 实现了接口。
关于 proxy-target-class 属性的用途这里就说完了,下面我们来看看两种不同创建动态代理的方式。
2.2 动态代理
2.2.1 基于 JDK 的动态代理
基于 JDK 的动态代理主要是通过 JDK 提供的代理创建类 Proxy 为目标对象创建代理,下面我们来看一下 Proxy 中创建代理的方法声明。如下:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
简单说一下上面的参数列表:
- loader - 类加载器
- interfaces - 目标类所实现的接口列表
- h - 用于封装代理逻辑
JDK 动态代理对目标类是有一定要求的,即要求目标类必须实现了接口,JDK 动态代理只能为实现了接口的目标类生成代理对象。至于 InvocationHandler,是一个接口类型,定义了一个 invoke 方法。使用者需要实现该方法,并在其中封装代理逻辑。
关于 JDK 动态代理的介绍,就先说到这。下面我来演示一下 JDK 动态代理的使用方式,如下:
目标类定义:
public interface UserService {
void save(User user);
void update(User user);
}
public class UserServiceImpl implements UserService {
@Override
public void save(User user) {
System.out.println("save user info");
}
@Override
public void update(User user) {
System.out.println("update user info");
}
}
代理创建者定义:
public interface ProxyCreator {
Object getProxy();
}
public class JdkProxyCreator implements ProxyCreator, InvocationHandler {
private Object target;
public JdkProxyCreator(Object target) {
assert target != null;
Class<?>[] interfaces = target.getClass().getInterfaces();
if (interfaces.length == 0) {
throw new IllegalArgumentException("target class don`t implement any interface");
}
this.target = target;
}
@Override
public Object getProxy() {
Class<?> clazz = target.getClass();
// 生成代理对象
return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(System.currentTimeMillis() + " - " + method.getName() + " method start");
// 调用目标方法
Object retVal = method.invoke(target, args);
System.out.println(System.currentTimeMillis() + " - " + method.getName() + " method over");
return retVal;
}
}
如上,invoke 方法中的代理逻辑主要用于记录目标方法的调用时间,和结束时间。下面写点测试代码简单验证一下,如下:
public class JdkProxyCreatorTest {
@Test
public void getProxy() throws Exception {
ProxyCreator proxyCreator = new JdkProxyCreator(new UserServiceImpl());
UserService userService = (UserService) proxyCreator.getProxy();
System.out.println("proxy type = " + userService.getClass());
System.out.println();
userService.save(null);
System.out.println();
userService.update(null);
}
}
测试结果如下:
如上,从测试结果中。我们可以看出,我们的代理逻辑正常执行了。另外,注意一下 userService 指向对象的类型,并非是 xyz.coolblog.proxy.UserServiceImpl,而是 com.sun.proxy.$Proxy4。
关于 JDK 动态代理,这里先说这么多。下一节,我来演示一下 CGLIB 动态代理,继续往下看吧。
2.2.2 基于 CGLIB 的动态代理
当我们要为未实现接口的类生成代理时,就无法使用 JDK 动态代理了。那么此类的目标对象生成代理时应该怎么办呢?当然是使用 CGLIB 了。在 CGLIB 中,代理逻辑是封装在 MethodInterceptor 实现类中的,代理对象则是通过 Enhancer 类的 create 方法进行创建。下面我来演示一下 CGLIB 创建代理对象的过程,如下:
本节的演示环节,打算调侃(无贬低之意)一下59式坦克
,这是我们国家大量装备过的一款坦克。59式坦克有很多种改款,一般把改款统称为59改
,59改这个梗也正是源于此。下面我们先来一览59式坦克
的风采:
图片来源:百度图片搜索
下面我们的工作就是为咱们的 59 创建一个代理,即 59改。好了,开始我们的魔改吧。
目标类,59式坦克:
public class Tank59 {
void run() {
System.out.println("极速前行中....");
}
void shoot() {
System.out.println("轰...轰...轰...轰...");
}
}
CGLIB 代理创建者
public class CglibProxyCreator implements ProxyCreator {
private Object target;
private MethodInterceptor methodInterceptor;
public CglibProxyCreator(Object target, MethodInterceptor methodInterceptor) {
assert (target != null && methodInterceptor != null);
this.target = target;
this.methodInterceptor = methodInterceptor;
}
@Override
public Object getProxy() {
Enhancer enhancer = new Enhancer();
// 设置代理类的父类
enhancer.setSuperclass(target.getClass());
// 设置代理逻辑
enhancer.setCallback(methodInterceptor);
// 创建代理对象
return enhancer.create();
}
}
方法拦截器 - 坦克再制造:
public class TankRemanufacture implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
if (method.getName().equals("run")) {
System.out.println("正在重造59坦克...");
System.out.println("重造成功,已获取 ✨59改 之 超音速飞行版✨");
System.out.print("已起飞,正在突破音障。");
methodProxy.invokeSuper(o, objects);
System.out.println("已击落黑鸟 SR-71,正在返航...");
return null;
}
return methodProxy.invokeSuper(o, objects);
}
}
好了,下面开始演示,测试代码如下:
public class CglibProxyCreatorTest {
@Test
public void getProxy() throws Exception {
ProxyCreator proxyCreator = new CglibProxyCreator(new Tank59(), new TankRemanufacture());
Tank59 tank59 = (Tank59) proxyCreator.getProxy();
System.out.println("proxy class = " + tank59.getClass() + "\n");
tank59.run();
System.out.println();
System.out.print("射击测试:");
tank59.shoot();
}
}
测试结果如下:
如上,"极速前行中...." 和 "轰...轰...轰...轰..." 这两行字符串是目标对象中的方法打印出来的,其他的则是由代理逻辑打印的。由此可知,我们的代理逻辑生效了。
好了,最后我们来看一下,经过魔改后的 59,也就是超音速59改
的效果图:
图片来源:未知
本节用59式坦克举例,仅是调侃,并无恶意。作为年轻的一代,我们应感谢那些为国防事业做出贡献的科技人员们。没有他们贡献,我们怕是不会有像今天这样安全的环境了(尽管不完美)。
到此,背景知识就介绍完了。下一章,我将开始分析源码。源码不是很长,主逻辑比较容易懂,所以一起往下看吧。
3.源码分析
为目标 bean 创建代理对象前,需要先创建 AopProxy 对象,然后再调用该对象的 getProxy 方法创建实际的代理类。我们先来看看 AopProxy 这个接口的定义,如下:
public interface AopProxy {
/** 创建代理对象 */
Object getProxy();
Object getProxy(ClassLoader classLoader);
}
在 Spring 中,有两个类实现了 AopProxy,如下:
Spring 在为目标 bean 创建代理的过程中,要根据 bean 是否实现接口,以及一些其他配置来决定使用 AopProxy 何种实现类为目标 bean 创建代理对象。下面我们就来看一下代理创建的过程,如下:
protected Object createProxy(
Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {
if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
/*
* 默认配置下,或用户显式配置 proxy-target-class = "false" 时,
* 这里的 proxyFactory.isProxyTargetClass() 也为 false
*/
if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
/*
* 检测 beanClass 是否实现了接口,若未实现,则将
* proxyFactory 的成员变量 proxyTargetClass 设为 true
*/
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
// specificInterceptors 中若包含有 Advice,此处将 Advice 转为 Advisor
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}
// 创建代理
return proxyFactory.getProxy(getProxyClassLoader());
}
public Object getProxy(ClassLoader classLoader) {
// 先创建 AopProxy 实现类对象,然后再调用 getProxy 为目标 bean 创建代理对象
return createAopProxy().getProxy(classLoader);
}
getProxy 这里有两个方法调用,一个是调用 createAopProxy 创建 AopProxy 实现类对象,然后再调用 AopProxy 实现类对象中的 getProxy 创建代理对象。这里我们先来看一下创建 AopProxy 实现类对象的过程,如下:
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
/*
* 下面的三个条件简单分析一下:
*
* 条件1:config.isOptimize() - 是否需要优化,这个属性没怎么用过,
* 细节我不是很清楚
* 条件2:config.isProxyTargetClass() - 检测 proxyTargetClass 的值,
* 前面的代码会设置这个值
* 条件3:hasNoUserSuppliedProxyInterfaces(config)
* - 目标 bean 是否实现了接口
*/
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 创建 CGLIB 代理,ObjenesisCglibAopProxy 继承自 CglibAopProxy
return new ObjenesisCglibAopProxy(config);
}
else {
// 创建 JDK 动态代理
return new JdkDynamicAopProxy(config);
}
}
}
如上,DefaultAopProxyFactory 根据一些条件决定生成什么类型的 AopProxy 实现类对象。生成好 AopProxy 实现类对象后,下面就要为目标 bean 创建代理对象了。这里以 JdkDynamicAopProxy 为例,我们来看一下,该类的 getProxy 方法的逻辑是怎样的。如下:
public Object getProxy() {
return getProxy(ClassUtils.getDefaultClassLoader());
}
public Object getProxy(ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
// 调用 newProxyInstance 创建代理对象
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
如上,请把目光移至最后一行有效代码上,会发现 JdkDynamicAopProxy 最终调用 Proxy.newProxyInstance 方法创建代理对象。到此,创建代理对象的整个过程也就分析完了,不知大家看懂了没。好了,关于创建代理的源码分析,就先说到这里吧。
4.总结
本篇文章对 Spring AOP 创建代理对象的过程进行了较为详细的分析,并在分析源码前介绍了相关的背景知识。总的来说,本篇文章涉及的技术点不是很复杂,相信大家都能看懂。限于个人能力,若文中有错误的地方,欢迎大家指出来。好了,本篇文章到此结束,谢谢阅读。
参考
附录:Spring 源码分析文章列表
Ⅰ. IOC
更新时间 | 标题 |
---|---|
2018-05-30 | Spring IOC 容器源码分析系列文章导读 |
2018-06-01 | Spring IOC 容器源码分析 - 获取单例 bean |
2018-06-04 | Spring IOC 容器源码分析 - 创建单例 bean 的过程 |
2018-06-06 | Spring IOC 容器源码分析 - 创建原始 bean 对象 |
2018-06-08 | Spring IOC 容器源码分析 - 循环依赖的解决办法 |
2018-06-11 | Spring IOC 容器源码分析 - 填充属性到 bean 原始对象 |
2018-06-11 | Spring IOC 容器源码分析 - 余下的初始化工作 |
Ⅱ. AOP
更新时间 | 标题 |
---|---|
2018-06-17 | Spring AOP 源码分析系列文章导读 |
2018-06-20 | Spring AOP 源码分析 - 筛选合适的通知器 |
2018-06-20 | Spring AOP 源码分析 - 创建代理对象 |
2018-06-22 | Spring AOP 源码分析 - 拦截器链的执行过程 |
Ⅲ. MVC
更新时间 | 标题 |
---|---|
2018-06-29 | Spring MVC 原理探秘 - 一个请求的旅行过程 |
2018-06-30 | Spring MVC 原理探秘 - 容器的创建过程 |
本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处
作者:田小波
本文同步发布在我的个人博客:http://www.tianxiaobo.com
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。
Spring AOP 源码分析 - 创建代理对象的更多相关文章
- Spring AOP 源码分析 - 拦截器链的执行过程
1.简介 本篇文章是 AOP 源码分析系列文章的最后一篇文章,在前面的两篇文章中,我分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程.现在我们的得 ...
- Spring AOP 源码分析 - 筛选合适的通知器
1.简介 从本篇文章开始,我将会对 Spring AOP 部分的源码进行分析.本文是 Spring AOP 源码分析系列文章的第二篇,本文主要分析 Spring AOP 是如何为目标 bean 筛选出 ...
- Spring AOP 源码分析系列文章导读
1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解.在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅 ...
- Spring AOP源码分析(三):基于JDK动态代理和CGLIB创建代理对象的实现原理
AOP代理对象的创建 AOP相关的代理对象的创建主要在applyBeanPostProcessorsBeforeInstantiation方法实现: protected Object applyBea ...
- 5.2 Spring5源码--Spring AOP源码分析二
目标: 1. 什么是AOP, 什么是AspectJ 2. 什么是Spring AOP 3. Spring AOP注解版实现原理 4. Spring AOP切面原理解析 一. 认识AOP及其使用 详见博 ...
- 5.2 spring5源码--spring AOP源码分析二--切面的配置方式
目标: 1. 什么是AOP, 什么是AspectJ 2. 什么是Spring AOP 3. Spring AOP注解版实现原理 4. Spring AOP切面原理解析 一. 认识AOP及其使用 详见博 ...
- spring AOP源码分析(三)
在上一篇文章 spring AOP源码分析(二)中,我们已经知道如何生成一个代理对象了,那么当代理对象调用代理方法时,增强行为也就是拦截器是如何发挥作用的呢?接下来我们将介绍JDK动态代理和cglib ...
- spring aop 源码分析(三) @Scope注解创建代理对象
一.源码环境的搭建: @Component @Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON,proxyMode = ScopedP ...
- spring aop 源码分析(二) 代理方法的执行过程分析
在上一篇aop源码分析时,我们已经分析了一个bean被代理的详细过程,参考:https://www.cnblogs.com/yangxiaohui227/p/13266014.html 本次主要是分析 ...
随机推荐
- ueditor 上传图片
ueditor在配置图片,附件上传 首先,是以web项目为基础的,需要安装好eclipse以及tomcat 其次,需要下载ueditor(可去百度官网下载 http://ueditor.baidu. ...
- NOIP2016原题终结测试(2017081801)
NOIP2016还有几道原题没有写掉,今天就一并布置掉. 答案的问题,有部分会先放到NOIP题解中,是单独发布的. 最后会汇总放在答案中,各位不要急. 还有,后期会有原创题测试,这个不急,反正11月才 ...
- LD_LIBRARY_PATH
LD_LIBRARY_PATH是Linux环境变量名,该环境变量主要用于指定查找共享库(动态链接库)时除了默认路径之外的其他路径. 在linux下可以用export命令来设置这个值,比如 在linux ...
- centos6 搭建nginx实现负载均衡
一.安装nginx 1)准备2台服务器,环境一样,同时执行 rpm -ivh http://mirrors.aliyun.com/epel/epel-release-latest-6.noarch.r ...
- php获取响应状态码
$ch = curl_init('http://www.jb51.net'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_exec($ch); ...
- __getitem__()、__setitem__()与__delitem__()
# 如果想要运用[]取值,可以实现__getitem__() # 想要运用[]设值,可以实现__setitem__() # 若想通过del与[]来删除,可以实现__delitem__() class ...
- idea关于tab的设置
新手使用,一不小心tab显示在右面了,这不学习下给搞正常点. settings===>Editor=====>Editor Tabs; Palacement设置的是tab显示的部位: Ta ...
- 以太坊虚拟机(EVM)
转载链接:https://ethfans.org/posts/solidity-chapter1-introduciton-to-smart-contracts 概括总览: 以太坊虚拟机(EVM)是以 ...
- 链家web前端面试
共有三轮面试,每个面试官的第一个问题都是:介绍一个你觉着比较出彩的项目 第一轮面试: 因为公司项目没什么亮点,很传统的pc端,美女面试官就说让讲一下我用react的私人项目; 问了很多都是关于reac ...
- 5.Vue临时上传文件夹
1.在项目目录中,通过npm install multiparty进行安装必要组件npm install multiparty --save-dev 2.app.js中添加app.use(bodyPa ...