首页 > 编程语言 >【Spring源码分析】Spring Scope功能中的动态代理 - Scoped Proxy

【Spring源码分析】Spring Scope功能中的动态代理 - Scoped Proxy

时间:2024-08-06 18:28:21浏览次数:13  
标签:Spring 代理 targetBeanName Bean bean 源码 Proxy 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。这个参数一共有四个取值:

  1. DEFAULT:默认值,等同于NO
  2. NO:不创建scoped proxy,对于非单例Scope的bean来说此模式通常没用;
  3. INTERFACES:使用JDK动态代理创建一个基于接口实现的动态代理对象;
  4. TARGET_CLASS:使用CGLIB创建一个基于继承的动态代理对象。
    可见,@RefreshScope 默认使用了基于继承实现的动态代理对象。这样做有几点好处:
  5. 在使用方注入时既可使用接口注入,也可以使用类型注入。如果 proxyMode = ScopedProxyMode.INTERFACES,创建出的 scoped proxy 类型并非原Bean的类型,而只是实现了它所实现的接口。由于 @Autowired 真正要注入的是 scoped proxy,如果变量定义为 Bean 的类型,Spring 会报 No qualifying bean of type '...' available 错误;
  6. 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 的工作逻辑:

  1. @Autowired注入的是一个 scoped proxy,它的 BeanDefinition 的 scope 其实是 singleton;
  2. 调用 bean 方法时,scoped proxy 在 Spring 容器中获取名为 scopedTarget.<originalBeanName> 的被代理 bean,此 bean 的 scope 是 refresh;
  3. 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,代理,targetBeanName,Bean,bean,源码,Proxy,proxy
From: https://www.cnblogs.com/flyingblu-space/p/18344004/implementation_of_spring_scoped_proxy

相关文章

  • 【深入剖析】Spring依赖注入的最佳实践(@Autowired的正确用法)
    文章目录为什么Spring不推荐使用@Autowired进行字段注入?字段注入的使用与弊端1.不可见的依赖关系2.无法使用final修饰符3.测试不便推荐的替代方案1.构造器注入构造器注入的优势包括2.设值注入设值注入的优势包括总结为什么Spring不推荐使用@Autowired进行字......
  • springblade技术架构
    1.前后端的下载运行与对接SpringBlade源码下载地址https://gitee.com/smallc/SpringBlade打开终端,事先准备好一个空文件夹创建project文件夹在project文件夹下创建cloud、boot、vue文件夹进入cloud执行gitclone命令gitclonehttps://gitee.com/smallc/SpringBlade.git下......
  • Springboot计算机毕业设计电影推荐网站0unwo
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,电影分类,电影信息,通知公告,电影资讯开题报告内容一、研究背景与意义随着互联网技术的飞速发展,在线娱乐已成为人们日常生活中不可或缺的一部分。电影作为......
  • Springboot计算机毕业设计电商订单管理系统(程序+源码+数据库)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,商品分类,商家,商品信息开题报告内容摘要本文旨在设计并实现一个高效、易用的电商订单管理系统,以满足现代电商企业对订单处理、库存控制、物流跟踪及财务......
  • Springboot计算机毕业设计电脑商城购物系统(数据库、调试部署、开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,商品分类,商品品牌,商品信息开题报告内容1.选题背景及意义1.1选题背景随着计算机和网络的普及,电子商务已经成为现代社会不可或缺的一部分。特别是在21......
  • Spring Boot 中使用 JSON Schema 来校验复杂JSON数据
    JSON是我们编写API时候用于数据传递的常用格式,那么你是否知道JSONSchema呢?在数据交换领域,JSONSchema以其强大的标准化能力,为定义和规范JSON数据的结构与规则提供了有力支持。通过一系列精心设计的关键字,JSONSchema能够详尽地描述数据的各项属性。然而,仅凭JSONSchema......
  • Springboot计算机毕业设计电影评论网站8i2tp
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,电影分类,电影信息,电影资讯开题报告内容一、研究背景与意义随着互联网和移动设备的普及,人们对电影的观影需求不断增加。然而,在众多电影作品中选择适合自......
  • Springboot计算机毕业设计电商平台设计与实现(程序+源码+数据库)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表用户,产品类型,农产品信息开题报告内容一、研究背景与意义1.1研究背景随着互联网技术的迅猛发展和全球数字化进程的加速,电子商务已成为现代商业活动的重要组......
  • Springboot计算机毕业设计党支部信息管理系统的设计与实现(数据库、调试部署、开发环境
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表党员,党支部公告,党支部新闻,月缴费情况,年缴费情况,活动信息,支部信息,群众,入党申请,财务,负责人开题报告内容一、研究背景与意义研究背景随着信息技术的迅......
  • 基于K210智能人脸识别+车牌识别系统(完整工程资料源码)
    运行效果:基于K210的智能人脸与车牌识别系统工程目录:运行效果:目录:前言:一、国内外研究现状与发展趋势二、相关技术基础2.1人脸识别技术2.2车牌识别技术三、智能小区门禁系统设计3.1系统设计方案3.2系统设计目标3.3智能小区门禁系统硬件设计3.3.1控制模块......