@Scope与@RefreshScope注解

news2025/1/18 1:55:55

在SpringIOC中,我们熟知的BeanScope有单例(singleton)、原型(prototype), Bean的Scope影响了Bean的管理方式,例如创建Scope=singleton的Bean时,IOC会保存实例在一个Map中,保证这个Bean在一个IOC上下文有且仅有一个实例。SpringCloud新增了一个refresh范围的scope,同样用了一种独特的方式改变了Bean的管理方式,使得其可以通过外部化配置(.properties)的刷新,在应用不需要重启的情况下热加载新的外部化配置的值。
那么这个scope是如何做到热加载的呢?RefreshScope主要做了以下动作:
单独管理Bean生命周期创建Bean的时候如果是RefreshScope就缓存在一个专门管理的ScopeMap中,这样就可以管理Scope是Refresh的Bean的生命周期了重新创建Bean外部化配置刷新之后,会触发一个动作,这个动作将上面的ScopeMap中的Bean清空,这样,这些Bean就会重新被IOC容器创建一次,使用最新的外部化配置的值注入类中,达到热加载新值的效果下面我们深入源码,来验证我们上述的讲法。

@Scope注解

Spring管理的Bean默认是单例的
@Scope (“prototype”) 通过注解可以实现多个实例的解决
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:
单例( singleton):在整个应用中,只创建bean的一个实例。也就是单例
原型(prototype):每次注入或者通过Spring应用上下文获取的时候:getBean,都会创建一个新的bean实例。多例,每次getBean的时候都会创建新的对象
request表示请求,即在一次http请求中,被注解的Bean都是同一个Bean,不同的请求是不同的Bean;
session表示会话,即在同一个会话中,被注解的Bean都是使用的同一个Bean,不同的会话使用不同的Bean。

创建一个Bean的时候,会去BeanFactory的doGetBean方法创建Bean,不同scope有不同的创建方式:
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
                          @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

  //....

  // Create bean instance.
  // 单例Bean的创建
  if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      try {
        return createBean(beanName, mbd, args);
      }
      //...
    });
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
  }

  // 原型Bean的创建
  else if (mbd.isPrototype()) {
    // It's a prototype -> create a new instance.
    // ...
    try {
      prototypeInstance = createBean(beanName, mbd, args);
    }
    //...
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
  }

  else {
    // 由上面的RefreshScope注解可以知道,这里scopeName=refresh
    String scopeName = mbd.getScope();
    // 获取Refresh的Scope对象
    final Scope scope = this.scopes.get(scopeName);
    if (scope == null) {
      throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
    }
    try {
      // 让Scope对象去管理Bean
      Object scopedInstance = scope.get(beanName, () -> {
        beforePrototypeCreation(beanName);
        try {
          return createBean(beanName, mbd, args);
        }
        finally {
          afterPrototypeCreation(beanName);
        }
      });
      bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
    }
    //...
  }
}
//...
}

//...
}

这里可以看到几件事情:
单例和原型scope的Bean是硬编码单独处理的
除了单例和原型Bean,其他Scope是由Scope对象处理的
具体创建Bean的过程都是由IOC做的,只不过Bean的获取是通过Scope对象
通过scopeName获取对应的scope实例

@RefreshScope刷新bean

这里scope.get获取的Scope对象为RefreshScope,可以看到,创建Bean还是由IOC来做(createBean方法),但是获取Bean,都由RefreshScope对象的get方法去获取,其get方法在父类GenericScope中实现:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 将Bean缓存下来
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 创建Bean,只会创建一次,后面直接返回创建好的Bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

首先这里将Bean包装起来缓存下来

这里scope.get获取的Scope对象为RefreshScope,可以看到,创建Bean还是由IOC来做(createBean方法),但是获取Bean,都由RefreshScope对象的get方法去获取,其get方法在父类GenericScope中实现。bean的生命周期也由GenericScope控制

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 将Bean缓存下来
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 创建Bean,只会创建一次,后面直接返回创建好的Bean
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}

private final ScopeCache cache;
// 这里进入上面的 BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
public BeanLifecycleWrapper put(String name, BeanLifecycleWrapper value) {
  return (BeanLifecycleWrapper) this.cache.put(name, value);
}

这里的ScopeCache对象其实就是一个HashMap:

public class StandardScopeCache implements ScopeCache {

  private final ConcurrentMap<String, Object> cache = new ConcurrentHashMap<String, Object>();

  //...

  public Object get(String name) {
    return this.cache.get(name);
  }

  // 如果不存在,才会put进去
  public Object put(String name, Object value) {
    // result若不等于null,表示缓存存在了,不会进行put操作
    Object result = this.cache.putIfAbsent(name, value);
    if (result != null) {
      // 直接返回旧对象
      return result;
    }
    // put成功,返回新对象
    return value;
  }
}

这里就是将Bean包装成一个对象,缓存在一个Map中,下次如果再GetBean,还是那个旧的BeanWrapper。回到Scope的get方法,接下来就是调用BeanWrapper的getBean方法:

private Object bean;

public Object getBean() {
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

可以看出来,BeanWrapper中的bean变量即为实际Bean,如果第一次get肯定为空,就会调用BeanFactory的createBean方法创建Bean,创建出来之后就会一直保存下来。
由此可见,RefreshScope管理了Scope=Refresh的Bean的生命周期。
重新创建RefreshBean
当配置中心刷新配置之后,有两种方式可以动态刷新Bean的配置变量值,(SpringCloud-Bus还是Nacos差不多都是这么实现的):
向上下文发布一个RefreshEvent事件
Http访问/refresh这个EndPoint
不管是什么方式,最终都会调用ContextRefresher这个类的refresh方法,那么我们由此为入口来分析一下,热加载配置的原理:

// 这就是我们上面一直分析的Scope对象(实际上可以看作一个保存refreshBean的Map)
private RefreshScope scope;

public synchronized Set<String> refresh() {
  // 更新上下文中Environment外部化配置值
  Set<String> keys = refreshEnvironment();
  // 调用scope对象的refreshAll方法
  this.scope.refreshAll();
  return keys;
}

我们一般是使用@Value、@ConfigurationProperties去获取配置变量值,其底层在IOC中则是通过上下文的Environment对象去获取property值,然后依赖注入利用反射Set到Bean对象中去的。

那么如果我们更新Environment里的Property值,然后重新创建一次RefreshBean,再进行一次上述的依赖注入,是不是就能完成配置热加载了呢?@Value的变量值就可以加载为最新的了。

这里说的刷新Environment对象并重新依赖注入则为上述两个方法做的事情:

Set keys = refreshEnvironment();
this.scope.refreshAll();

刷新Environment对象

刷新环境遍历指的的是将配置替换到当前的Environment,后面如果再根据配置创建对象就会使用新的配置设置属性。
例如org.springframework.cloud.endpoint.event.RefreshEventListener进行将配置文件刷新进入environment中的操作。

ConfigurableApplicationContext addConfigFilesToEnvironment() {
	StandardEnvironment environment = copyEnvironment(
					this.context.getEnvironment());
			SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
					.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
					.environment(environment);
			// Just the listeners that affect the environment (e.g. excluding logging
			// listener because it has side effects)
			builder.application()
					.setListeners(Arrays.asList(new BootstrapApplicationListener(),
							new ConfigFileApplicationListener()));
			capture = builder.run();
 }


可以看到,这里归根结底就是SpringBoot启动上下文那种方法,新做了一个Spring上下文,因为Spring启动后会对上下文中的Environment进行初始化,获取最新配置,所以这里利用Spring的启动,达到了获取最新的Environment对象的目的。然后去替换旧的上下文中的Environment对象中的配置值即可。

重新创建RefreshBean

经过上述刷新Environment对象的动作,此时上下文中的配置值已经是最新的了。思路回到ContextRefresher的refresh方法,接下来会调用Scope对象的refreshAll方法:

public void refreshAll() {
  // 销毁Bean
  super.destroy();
  this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

public void destroy() {
  List<Throwable> errors = new ArrayList<Throwable>();
  // 缓存清空
  Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
  // ...
}

还记得上面的管理RefreshBean生命周期那一节关于缓存的讨论吗,cache变量是一个Map保存着RefreshBean实例,这里直接就将Map清空了。

思路回到BeanFactory的doGetBean的流程中,从IOC容器中获取RefreshBean是交给RefreshScope的get方法做的:

public Object get(String name, ObjectFactory<?> objectFactory) {
  // 由于刚刚清空了缓存Map,这里就会put一个新的BeanLifecycleWrapper实例
  BeanLifecycleWrapper value = this.cache.put(name,
                                              new BeanLifecycleWrapper(name, objectFactory));
  this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
  try {
    // 在这里是新的BeanLifecycleWrapper实例调用getBean方法
    return value.getBean();
  }
  catch (RuntimeException e) {
    this.errors.put(name, e);
    throw e;
  }
}
public Object getBean() {
  // 由于是新的BeanLifecycleWrapper实例,这里一定为null
  if (this.bean == null) {
    synchronized (this.name) {
      if (this.bean == null) {
        // 调用IOC容器的createBean,再创建一个Bean出来
        this.bean = this.objectFactory.getObject();
      }
    }
  }
  return this.bean;
}

可以看到,此时RefreshBean被IOC容器重新创建一个出来了,经过IOC的依赖注入功能,@Value的就是一个新的配置值了。到这里热加载功能实现基本结束。

根据以上分析,我们可以看出只要每次我们都从IOC容器中getBean,那么拿到的RefreshBean一定是带有最新配置值的Bean。

@RefreshScope代理对象

  • @Scope 的注册 AnnotatedBeanDefinitionReader#registerBean
  public void registerBean(...){
    ...
    ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
      abd.setScope(scopeMetadata.getScopeName());
    ...
      definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
  }
  • 读取@Scope元数据, AnnotationScopeMetadataResolver#resolveScopeMetadata
public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) {
          AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(
                  annDef.getMetadata(), Scope.class);
          if (attributes != null) {
              metadata.setScopeName(attributes.getString("value"));
              ScopedProxyMode proxyMode = attributes.getEnum("proxyMode");
              if (proxyMode == null || proxyMode == ScopedProxyMode.DEFAULT) {
                  proxyMode = this.defaultProxyMode;
              }
              metadata.setScopedProxyMode(proxyMode);
          }
}
  • Scope实例对象通过ScopedProxyFactoryBean创建,其中通过AOP使其实现ScopedObject接口,这里不再展开

在这里插入图片描述

每次使用@RefreshScope的bean的get方法时都会重新通过this.beanFactory.getBean(this.targetBeanName);
如果被清空了的话,那么会重新创建bean会使用,刷新后的environment的配置注入属性,实现动态刷新。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/12149.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nest.js创建以及error相关问题

开始之前&#xff0c;你可以使用 Nest CLI 创建项目&#xff0c;也可以克隆一个 starter project&#xff08;两者的结果是一样的&#xff09;。 若要使用 Nest CLI 构建项目&#xff0c;请运行以下命令。这将创建一个新的项目目录&#xff0c;并使用核心的 Nest 文件和支撑模…

我把 CPU 三级缓存的秘密,藏在这 8 张图里

本文已收录到 GitHub AndroidFamily&#xff0c;有 Android 进阶知识体系&#xff0c;欢迎 Star。技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 进 Android 面试交流群。 前言 大家好&#xff0c;我是小彭。 在上一篇文章里&#xff0c;我们聊到了计算机存储器系统的金…

盘点机PDA搭配蓝牙便携打印机,条码标签打印,超市仓库条码管理,条码标签纸

null使用盘点机PDA&#xff0c;搭配蓝牙便携打印机&#xff0c;移动打印条码标签的操作和设置。对于商品本身没有条码的商品&#xff0c;比如&#xff1a;外购回来无条码的商品&#xff0c;工厂自己生产出来的成品&#xff0c;那么这种就需要打印商品条码进行粘贴&#xff0c;即…

Spring Security认证之登录表单配置

本文内容来自王松老师的《深入浅出Spring Security》&#xff0c;自己在学习的时候为了加深理解顺手抄录的&#xff0c;有时候还会写一些自己的想法。 自定义登录页面 文接上篇&#xff0c;这一篇学习如何自定义登录表单。我们创建一个Spring Boot项目之后&#xff0c;还是一样…

windows docker 及 k8s 环境搭建

docker 环境搭建 下载 docker 下载 docker for desktop&#xff0c; , 配置 镜像源 开通 kubenates 功能 注册一个 docker hub 账号 记住账号密码&#xff0c; 将来拉取镜像要用到&#xff0c; 在 docker for desktop 中登录该账号 kubenates 环境搭建 docker 中开通 k8…

nginx服务器

一、介绍 Nginx (engine x) 是一个高性能的HTTP和反向代理服务器&#xff0c;也是一个IMAP/POP3/SMTP服务。Nginx是由伊戈尔赛索耶夫为俄罗斯访问量第二的Rambler.ru站点&#xff08;俄文&#xff1a;Рамблер&#xff09;开发的&#xff0c;第一个公开版本0.1.0发布于2…

用户管理系统(2)

2.实现添加功能:有两个和后端交互的接口 根据用户的身份显示登陆界面: 进行插入操作: 1)在我们的前端直接获取到用户名&#xff0c;密码&#xff0c;确认密码&#xff0c;年龄&#xff0c;QQ&#xff0c;邮箱&#xff0c;判断他们是否为空&#xff0c;检测密码和确认密码是否一…

docker 第二次学习笔记

一、dockers简介 docker官网&#xff1a;https://www.docker.com 1.1 docker定义 docker是一种容器化技术&#xff0c;用来更好的构建和发布应用。 二、docker安装 2.1 方法1 centos7.x系统的安装 官网安装步骤&#xff1a;https://docs.docker.com/engine/install/cento…

IDEA中,maven项目下,lombok插件 ,添加lombok.jar, Maven项目下lombok依赖配置

IDEA中&#xff0c;maven项目下&#xff0c;lombok插件 &#xff0c;添加lombok.jar, Maven项目下lombok依赖配置 Maven 项目的创建 在IDEA 21版中&#xff0c;Maven项目无需下载其他版本&#xff0c;查看有无Maven&#xff0c;如果没有下载 安装 首先打开IDEA &#xff0c;点…

​ 详解Linux内核通信-proc文件系统

使用 /proc 文件系统来访问 Linux 内核的内容&#xff0c;这个虚拟文件系统 在内核空间和用户空间之间打开了一个通信窗口&#xff1a; /proc 文件系统是一个虚拟文件系统&#xff0c;通过它可以使用一种新的方法在 Linux内核空间和用户间之间进行通信。在 /proc 文件系统中&…

MySQL事务和索引

✏️作者&#xff1a;银河罐头 &#x1f4cb;系列专栏&#xff1a;MySQL &#x1f332;“种一棵树最好的时间是十年前&#xff0c;其次是现在” 目录索引概念使用索引在MySQL中的数据结构事务概念mysql的隔离级别索引 概念 索引是一种特殊的文件&#xff0c;包含着对数据表里所…

一图看懂,阿里云飞天企业版如何支持政企数智创新

杭州&#xff0c;2022年11月5日 – 今日&#xff0c;在云栖大会专有云技术和应用实践论坛&#xff0c;阿里云重磅发布飞天企业版在建云、管云、用云方面的全面升级&#xff0c;并邀请行业专家、政企客户代表和合作伙伴面向未来十年共话新一代政企IT发展趋势&#xff0c;分享阿里…

行业洞察 | AI贩卖的焦虑,我们该买单吗?

图片来源Midjourney Showcase 在过去的几个月里&#xff0c;人工智能生成的艺术在受欢迎程度和可访问性方面都经历了快速增长。随着DALL-E、Midjourney和Stable Diffusion等引擎刺激了 AI 生成的艺术品在在线平台上的大量涌入。 此前&#xff0c;一位美国39岁游戏设计师&#…

测试行业3年经验,面试想拿 15K,HR说你只值 7K,该如何回答或者反驳?

面试最尴尬的不是被拒绝&#xff0c;而是直接说你不值那个价格... 最近朋友在面试的时候&#xff0c;HR 突然来了句&#xff1a;你只值 7K。朋友后面和我说了这个事。我想如果是我处在这种情况下&#xff0c;自己并不能很好地回答或者反驳。不知道大家会怎么回答或者反驳&…

深入浅出了解MYSQL8特性注入是什么

前言 今天给大家带来的是MYSQL8版本的特性注入&#xff0c;说起SQL注入大家一定不陌生&#xff0c;可是你有没有想过&#xff0c;当SQL注入中最关键的函数SELECT被过滤后&#xff0c;我们要如何去执行SQL语句呢&#xff0c;这就是本文要讲的内容&#xff0c;即利用MYSQL8版本的…

智慧人社解决方案-最新全套文件

智慧人社解决方案-最新全套文件一、建设背景二、思路架构三、建设方案1、全局性数据整合2、综合数据分析平台3、一体化数据管控四、获取 - 智慧人社全套最新解决方案合集一、建设背景 智慧人社平台以建设智慧大社保服务体系为目标&#xff0c;全面践行“互联网&#xff0b;人社…

python基于PHP+MySQL的大学生交友社交网站

近年来,大学生的数量在逐步的增加,为了能够让这些大学生有一个更好的交友环境,需要创建一个基于大学生的社交交友网站。这样可以拉近彼此大学生之间的感情,让他们可以更好的进行学习和交流。 PHP大学生交友社交网站通过PHP&#xff1a;MySQL进行开发,分为前台和后台两部分,通过…

[附源码]SSM计算机毕业设计餐厅卫生安全系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

mariadb10.8 主主同步 相互复制

1. 配置说明 操作系统&#xff1a;CentOS7 CPU指令集&#xff1a;x86_64 host1 主机&#xff1a; 2 core 2G 40GIP&#xff1a;192.168.0.98 host2 主机&#xff1a; 2 core 2G 40GIP&#xff1a;192.168.0.166 2. 数据库安装 见 MariaDB官方下载文档 https://mariadb.or…

mysql“数据不存在插入,存在则更新”实现

参考文章:Mysql:如果数据存在则更新&#xff0c;不存在则插入 场景 工作中有遇到需要配置一些指定的字段数据&#xff0c;但数据量大&#xff0c;不清楚之前是否有配置过&#xff0c;正确的思路应该是如果有这条数据了&#xff0c;那么更新数据的值&#xff0c;如果没有这条数…