【Spring源码分析】Spring Scope功能中的动态代理 - Scoped Proxy
本文基于Springboot 3.3.2及Springcloud 2023.0.1版本编写。
Spring Scoped Proxy是什么
在使用Spring cloud配置中心动态配置更新功能时,笔者发现在给一个类加上@RefreshScope注解后,其中@Value注入的字段会被自动更新。起初笔者以为Spring在收到配置更新事件后会自动设置该bean的字段值,但测试后发现配置更新是通过重建整个bean的方式来实现的。实验代码如下:
@RestController
public class TestController {
@Autowired
private RefreshClazz refreshClazz;
@GetMapping("/test/url")
public String getCount() {
return "count=" + refreshClazz.getCount();
}
@Component
@RefreshScope
public static class RefreshClazz {
@Value("${example.config}")
private String configStr;
private int count = 0;
@PostConstruct
public void postConstruct() {
System.out.println("POST_CONSTRUCT");
}
@PreDestroy
public void preDestroy() {
System.out.println("PRE_DESTROY");
}
public int getCount() {
return count++;
}
}
}
代码中,RefreshClazz 是一个被标记了@RefreshScope的 Bean,通过@Autowired的方式注入到 Controller 中。运行上面的代码,会发现当配置更新后,RefreshClazz 的内部字段 count 被重置到了 0,同时也会输出 PRE_DESTROY 和 POST_CONSTRUCT,说明旧的 Bean 被删除,新的 Bean 被创建了。
那么问题来了,RefreshClazz是被静态注入到 controller 中的,如何做到自动刷新的呢?原理便是注入的是动态代理对象。Spring 在实现诸多功能(如@Lazy、@Transactional、@Cacheable)时都用到了动态代理,Scope 也是其中一个。Scope 功能用到的动态代理被称为 Scoped Proxy。
如何使用 Scoped Proxy
@RefreshScope定义代码:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
根据@RefreshScope的定义我们可以发现,它等同于@Scope(value = "refresh", proxyMode = ScopedProxyMode.TARGET_CLASS)。其关键在于proxyMode = ScopedProxyMode.TARGET_CLASS。这个参数一共有四个取值:
DEFAULT:默认值,等同于NO;NO:不创建scoped proxy,对于非单例Scope的bean来说此模式通常没用;INTERFACES:使用JDK动态代理创建一个基于接口实现的动态代理对象;TARGET_CLASS:使用CGLIB创建一个基于继承的动态代理对象。
可见,@RefreshScope默认使用了基于继承实现的动态代理对象。这样做有几点好处:- 在使用方注入时既可使用接口注入,也可以使用类型注入。如果
proxyMode = ScopedProxyMode.INTERFACES,创建出的 scoped proxy 类型并非原Bean的类型,而只是实现了它所实现的接口。由于@Autowired真正要注入的是 scoped proxy,如果变量定义为 Bean 的类型,Spring 会报No qualifying bean of type '...' available错误; - JDK实现的动态代理在性能和内存开销上稍大于CGLIB动态代理。
Scoped Proxy 是如何被创建的
Spring Bean 注册
在Spring容器中注册 scoped proxy 的逻辑来自 ScopedProxyUtils#createScopedProxy 方法,该方法的代码如下:
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
BeanDefinitionRegistry registry, boolean proxyTargetClass) {
// 获取原beanName和bean定义
String originalBeanName = definition.getBeanName();
BeanDefinition targetDefinition = definition.getBeanDefinition();
// 生成被代理bean的beanName
String targetBeanName = getTargetBeanName(originalBeanName);
// 创建动态代理Bean的BeanDefinition
RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
proxyDefinition.setSource(definition.getSource());
proxyDefinition.setRole(targetDefinition.getRole());
// 将被代理bean的beanName传给动态代理Bean
proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
if (proxyTargetClass) {
targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
// ScopedProxyFactoryBean's "proxyTargetClass" default is TRUE, so we don't need to set it explicitly here.
}
else {
proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
}
// 将原bean的属性复制到动态代理bean定义中.
proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
proxyDefinition.setPrimary(targetDefinition.isPrimary());
if (targetDefinition instanceof AbstractBeanDefinition abd) {
proxyDefinition.copyQualifiersFrom(abd);
}
// 将底层bean隐藏起来,不参与注入.
targetDefinition.setAutowireCandidate(false);
targetDefinition.setPrimary(false);
// 将底层bean的beanName设置为targetBeanName,注册到容器中.
registry.registerBeanDefinition(targetBeanName, targetDefinition);
// 返回刚生成的动态代理bean的BeanDefinition,beanName设置为原bean的名字.
return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}
在进入这个方法之前,这个被@Scope修饰的 Bean 和其他普通单例 Bean 并没有区别。但此方法对它进行了一通魔改,最后将原 Bean 的定义改了个名字藏在了 Spring 容器内部,而暴露出了一个新生成的 scoped proxy bean。因为新的 bean 名字和原 bean 名一样,并且可能是 Primary Bean,因此在 @Autowired 注入时默认就注入了这个动态代理 bean。
原 Bean 的名字被改成了什么呢?可以参考 getTargetBeanName() 方法:
private static final String TARGET_NAME_PREFIX = "scopedTarget.";
public static String getTargetBeanName(String originalBeanName) {
return TARGET_NAME_PREFIX + originalBeanName;
}
因此,底层 Bean 的 beanName 为 scopedTarget.<originalBeanName>。
动态代理对象生成
在 ScopedProxyUtils#createScopedProxy 方法的代码中,我们注意到新生成的动态代理 Bean 类被设置为了 ScopedProxyFactoryBean.class。这是一个 FactoryBean,负责具体生成动态代理对象。代码如下:
public class ScopedProxyFactoryBean extends ProxyConfig
implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean {
/** 从Spring容器中获取底层对象的TargetSource. */
private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
/** 底层bean的beanName. */
@Nullable
private String targetBeanName;
/** 缓存的单例 Scoped proxy. */
@Nullable
private Object proxy;
/** 构造方法. */
public ScopedProxyFactoryBean() {
setProxyTargetClass(true);
}
/** 设置底层bean的beanName. */
public void setTargetBeanName(String targetBeanName) {
this.targetBeanName = targetBeanName;
this.scopedTargetSource.setTargetBeanName(targetBeanName);
}
/** 创建动态代理对象的主方法. */
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (!(beanFactory instanceof ConfigurableBeanFactory cbf)) {
throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory);
}
// 为targetSource设置使用的beanFactory
this.scopedTargetSource.setBeanFactory(beanFactory);
// 通过ProxyFactory创建动态代理对象
ProxyFactory pf = new ProxyFactory();
pf.copyFrom(this);
// 使用配置好的targetSource
pf.setTargetSource(this.scopedTargetSource);
Assert.notNull(this.targetBeanName, "Property 'targetBeanName' is required");
Class<?> beanType = beanFactory.getType(this.targetBeanName);
if (beanType == null) {
throw new IllegalStateException("Cannot create scoped proxy for bean '" + this.targetBeanName +
"': Target type could not be determined at the time of proxy creation.");
}
if (!isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) {
pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader()));
}
// Add an introduction that implements only the methods on ScopedObject.
ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName());
pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));
// Add the AopInfrastructureBean marker to indicate that the scoped proxy
// itself is not subject to auto-proxying! Only its target bean is.
pf.addInterface(AopInfrastructureBean.class);
// 生成并缓存动态代理对象
this.proxy = pf.getProxy(cbf.getBeanClassLoader());
}
/** 工厂类的获取生成对象方法,获取生成的动态代理对象. */
@Override
@Nullable
public Object getObject() {
if (this.proxy == null) {
throw new FactoryBeanNotInitializedException();
}
return this.proxy;
}
}
这个类使用了 ProxyFactory 创建动态代理对象,它生成的动态代理对象通过接口 TargetSource 来获取代理的底层对象。上面的 ScopedProxyFactoryBean 使用了 SimpleBeanTargetSource,它的代码如下:
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
@Override
public Object getTarget() throws Exception {
return getBeanFactory().getBean(getTargetBeanName());
}
}
逻辑很清晰,通过 beanFactory 来获取名为 targetBeanName 的bean对象作为被代理的对象。而这个 targetBeanName 在Spring容器中注册 scoped proxy 的时候就被生成了,设置的逻辑是:
proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
Scoped Proxy 整体工作逻辑
看了前面的代码,我们可以总结出 scoped proxy 的工作逻辑:
- 被
@Autowired注入的是一个 scoped proxy,它的 BeanDefinition 的 scope 其实是 singleton; - 调用 bean 方法时,scoped proxy 在 Spring 容器中获取名为
scopedTarget.<originalBeanName>的被代理 bean,此 bean 的 scope 是 refresh; - scoped proxy 调用被代理 bean 的对应方法。
Scoped Bean 的生命周期
八股文里,Spring 有五种 Scope(singleton、prototype、request、session、globalSession)(这其实是过时的,最新文档里是六种:singleton、prototype、request、session、application、websocket)。那本文里的 refresh scope 是什么呢?
其实,只需要注入一个实现了 Scope 接口的 Bean,用户便可添加一个自定义的 scope。本文中的 refresh scope 就是 spring-cloud-context 中定义的。Spring 容器在获取任何不是 singleton 或 prototype 的 bean 时,会先找出该 scope 名所对应的 Scope 对象,再使用 Scope#get 从该对象中获取 bean,代码参考 AbstractBeanFactory#doGetBean。而 scoped bean 的生命周期便是由具体的 scope 类管理了(实现案例可以参考 GenericScope)。
【Spring源码分析】Spring Scope功能中的动态代理 - Scoped Proxy的更多相关文章
- Spring源码剖析5:JDK和cglib动态代理原理详解
AOP的基础是Java动态代理,了解和使用两种动态代理能让我们更好地理解 AOP,在讲解AOP之前,让我们先来看看Java动态代理的使用方式以及底层实现原理. 转自https://www.jiansh ...
- spring源码学习之【准备】jdk动态代理例子
一:被代理的对象所要实现的接口 package com.yeepay.testpoxy; import java.util.Map; /** * 被动态代理的接口 * @author shangxia ...
- cglib源码分析(四):cglib 动态代理原理分析
本文分下面三个部分来分析cglib动态代理的原理. cglib 动态代理示例 代理类分析 Fastclass 机制分析 一.cglib 动态代理示例 public class Target{ publ ...
- Java入门到精通——框架篇之Spring源码分析Spring两大核心类
一.Spring核心类概述. Spring里面有两个最核心的类这是Spring实现最重要的部分. 1.DefaultListableBeanFactory 这个类位于Beans项目下的org.spri ...
- spring源码学习之【准备】cglib动态代理例子
一:委托者 package com.yeepay.porxy.cglib.test; import java.util.HashMap; import java.util.Map; /** * 目标类 ...
- Spring源码分析——BeanFactory体系之抽象类、类分析(二)
上一篇分析了BeanFactory体系的2个类,SimpleAliasRegistry和DefaultSingletonBeanRegistry——Spring源码分析——BeanFactory体系之 ...
- 【Spring源码分析】原型Bean实例化过程、byName与byType及FactoryBean获取Bean源码实现
原型Bean加载过程 之前的文章,分析了非懒加载的单例Bean整个加载过程,除了非懒加载的单例Bean之外,Spring中还有一种Bean就是原型(Prototype)的Bean,看一下定义方式: & ...
- 【spring源码分析】IOC容器初始化(七)
前言:在[spring源码分析]IOC容器初始化(六)中分析了从单例缓存中加载bean对象,由于篇幅原因其核心函数 FactoryBeanRegistrySupport#getObjectFromFa ...
- spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor
更多文章点击--spring源码分析系列 主要分析内容: 一.BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor简述与demo示例 ...
- Spring源码分析(十八)创建bean
本文结合<Spring源码深度解析>来分析Spring 5.0.6版本的源代码.若有描述错误之处,欢迎指正. 目录 一.创建bean的实例 1. autowireConstructor 2 ...
随机推荐
- 网站_域名_DNS_端口_web访问过程
网站基本概念 服务器:能够提供服务器的机器,取决于机器上所安装的服务软件 web服务器:提供web服务(网站访问),需要安装web服务软件,Apache,tomcat,iis等 域名 (Domain ...
- Python3.7+Robot Framework+RIDE1.7.4.1安装使用教程
一.解惑:Robot Framewprk今天我们聊一聊,Robot Framework被众多测试工程师误会多年的秘密.今天我们一起来揭秘一下,最近经常在各大群里听到许多同行,在拿Robot Frame ...
- springboot拦截器@resource注解注入为null解决方案 拦截适配配置
springboot拦截器@resource注解注入为null解决方案 拦截适配配置 为什么@resource注入为nullinteceptor在springcontext之前加载,注入必然是nul ...
- StringUtils.join()方法使用
* StringUtils.join()方法使用 打印输出: * 使用 StringBuilder 进行拼接:张三,李四,王五 * 使用 StringUtils.join 进行拼接:张三,李四,王五 ...
- 一文了解Spark引擎的优势及应用场景
Spark引擎诞生的背景 Spark的发展历程可以追溯到2009年,由加州大学伯克利分校的AMPLab研究团队发起.成为Apache软件基金会的孵化项目后,于2012年发布了第一个稳定版本. 以下是S ...
- DHorse的配置文件
首先看一下DHorse的配置文件,如下: #============================================================================== ...
- Pytorch复制现有环境
一,在本机上,打开anaconda Prompt直接使用 conda create -n 新环境名 --clone 旧环境名
- Linux开机启动三种方式
有的时候,我们开机启动一些命令或者是一段脚本,又或者是开机启动自定义的服务. 下面归纳了2种实现的方式. 方式1-开机启动命令 vim /etc/rc.local #添加你想执行的命令 chmod + ...
- 一款EF Core下高性能、轻量级针对分表分库读写分离的解决方案
前言 今天大姚给大家分享一款EF Core下高性能.轻量级针对分表分库读写分离的解决方案,开源(Apache License)的EF Core拓展程序包:ShardingCore. ShardingC ...
- CF1860
很失败啊 A 题大力分讨,罚了 \(2\) 次 B 题大力分讨,罚了 \(1\) 次 C 题大力 dp 一发过 然后就睡觉了 感觉 CF 打少了智商掉了,被前几题拖了太久