SpringBoot @Scope与@RefreshScope注解使用详解

目录
  • 前言
  • @Scope注解
  • @RefreshScope刷新bean
  • 刷新Environment对象
  • 重新创建RefreshBean
  • @RefreshScope代理对象

前言

在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的配置注入属性,实现动态刷新。

到此这篇关于SpringBoot @Scope与@RefreshScope注解使用详解的文章就介绍到这了,更多相关SpringBoot @Scope与@RefreshScope内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • @RefreshScope在Quartz 触发器类导致异常问题解决分析

    目录 背景 问题 启示录 背景 承接上篇,测试过程中又遇到了 Nacos Config 的动态刷新注解 @RefreshScope 与 Quartz 框架结合的问题,Bug 排查路上,顺手记录一下吧. 问题 有个模块使用了Quartz ,通过配置控制任务调度的周期和分组名称. 因为引用了动态配置,所以对每个注入类都加上了 @RefreshScope ,代码如下: @Value("${quartz.task.cron}") private String taskCron; @Bean p

  • Spring Cloud @RefreshScope 原理及使用

    @RefreshScope那些事 要说清楚RefreshScope,先要了解Scope Scope(org.springframework.beans.factory.config.Scope)是Spring 2.0开始就有的核心的概念 RefreshScope(org.springframework.cloud.context.scope.refresh)是spring cloud提供的一种特殊的scope实现,用来实现配置.实例热加载. Scope -> GenericScope -> R

  • @RefreshScope 自动刷新配置文件的实例讲解

    1.在类上加@RefreshScope注解. 2.引入配置@Value. /** * @author 向振华 * @date 2018/12/17 17:20 */ @RefreshScope //配置文件自动刷新 @RestController @RequestMapping("test") public class TestController { @Value("${test.xzh}") //引入配置 private String xzh; @Request

  • SpringCloud的@RefreshScope 注解你了解吗

    目录 pom.xml properties 启动类 配置类 controller 打包 springcloud对应的springboot版本 参考: 总结 spring-boot-starter-actuator提供服务健康检查和暴露内置的url接口. spring-cloud-starter-config提供动态刷新的一些支持和注解. pom.xml <?xml version="1.0" encoding="UTF-8"?> <project

  • Springboot使用@RefreshScope注解实现配置文件的动态加载

    目录 pom.xml properties 启动类 配置类 controller 打包 springcloud对应的springboot版本 参考: spring-boot-starter-actuator提供服务健康检查和暴露内置的url接口. spring-cloud-starter-config提供动态刷新的一些支持和注解. pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xml

  • SpringBoot常见get/post请求参数处理、参数注解校验及参数自定义注解校验详解

    目录 springboot常见httpget,post请求参数处理 PathVaribale获取url路径的数据 RequestParam获取请求参数的值 注意 GET参数校验 POSTJSON参数校验 自定义注解校验 总结 spring boot 常见http get ,post请求参数处理 在定义一个Rest接口时通常会利用GET.POST.PUT.DELETE来实现数据的增删改查:这几种方式有的需要传递参数,后台开发人员必须对接收到的参数进行参数验证来确保程序的健壮性 GET一般用于查询数

  • SpringBoot 实现自定义的 @ConditionalOnXXX 注解示例详解

    目录 实现一个自定义的 @Conditional 派生注解 Conditional 派生注解的类如何注入到 spring 容器 实现一个自定义的 @Conditional 派生注解 自定义一个注解,继承 @Conditional 注解 // 派生注解 @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) @Documented @Conditional(CustomConditi

  • IntelliJ Idea SpringBoot 数据库增删改查实例详解

    SpringBoot 是 SpringMVC 的升级,对于编码.配置.部署和监控,更加简单 微服务 微服务是一个新兴的软件架构,就是把一个大型的单个应用程序和服务拆分为数十个的支持微服务.一个微服务的策略可以让工作变得更为简便,它可扩展单个组件而不是整个的应用程序堆栈,从而满足服务等级协议. Spring 为 微服务提供了一整套的组件-SpringClound , SpirngBoot 就是该基础. 第一个SpringBoot程序 这里使用的开发软件是IntelliJ Idea,和Eclipse

  • 使用SpringBoot整合ssm项目的实例详解

    SpringBoot是什么? Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程. Spring Boot 现在已经成为 Java 开发领域的一颗璀璨明珠,它本身是包容万象的,可以跟各种技术集成.成为 SpringBoot 全家桶,成为一把万能钥匙. SpringBoot的特点 1.创建独立的 Spring 应用程序 2.嵌入的 Tomcat ,无需部署 WAR 文件 3.简化 Maven 配置 4.自动配置 Spr

  • Mockito 结合 Springboot 进行应用测试的方法详解

    Spring Boot可以和大部分流行的测试框架协同工作:通过Spring JUnit创建单元测试:生成测试数据初始化数据库用于测试:Spring Boot可以跟BDD(Behavier Driven Development)工具.Cucumber和Spock协同工作,对应用程序进行测试. 在web应用程序中,我们主要是对Service层做单元测试,以前单元测试都是使用 junit4 ,对Controller层做集成测试或者接口测试,对Controller层的测试一般有两种方法:(1)发送htt

  • SpringBoot内置tomcat启动原理详解

    前言 不得不说SpringBoot的开发者是在为大众程序猿谋福利,把大家都惯成了懒汉,xml不配置了,连tomcat也懒的配置了,典型的一键启动系统,那么tomcat在springboot是怎么启动的呢? 内置tomcat 开发阶段对我们来说使用内置的tomcat是非常够用了,当然也可以使用jetty. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-bo

  • Springboot集成mybatis与jsp过程详解

    目录 什么是Spring Boot? springboot特点 springboot快速搭建项目 新建项目springboot_mybatis_jsp 项目配置 配置项目目录 配置工作目录(working directory) 配置pom.xml 配置application.properties 编写代码 建表t_user 编写User.java 编写UserMapper.xml 编写UserService.java.UserServiceImpl.java 编写Controller 什么是Sp

  • Spring Cloud动态配置刷新RefreshScope使用示例详解

    目录 引言 一.了解@RefreshScope,先要了解@Scope 二.RefreshScope 的实现原理 三.使用——@RefreshScope 使用流程 引言 用过Spring Cloud的同学都知道在使用动态配置刷新的我们要配置一个 @RefreshScope,在类上才可以实现对象属性的的动态更新. @RefreshScope 能实现动态刷新全仰仗着 @Scope这个注解. 一.了解@RefreshScope,先要了解@Scope 1.RefreshScope继承于GenericSco

  • SpringBoot Session接口验证实现流程详解

    目录 添加pom.xml 创建简单的测试接口 使用过滤器实现 使用拦截器实现 需求:只有用户登录成功后,才能访问其它接口,否则提示需要进行登录 项目仓库地址:https://gitee.com/aiw-nine/springboot_session_verify 添加pom.xml 新建Spring Boot(2.7.2)项目,添加如下依赖: <?xml version="1.0" encoding="UTF-8"?> <project xmlns

  • springboot单元测试两种方法实例详解

    这篇文章主要介绍了springboot单元测试两种方法实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 springboot的单元测试,这里介绍两种方式,一种是在测试类中添加注解:另一种是在代码中启动项目的main方法中继承接口(也可以写在其他方法中). 如 对查看数据库的连接池信息 进行单元测试 1. 在类上使用注解: @RunWith(SpringRunner.class) @SpringBootTest @RunWith(Sprin

随机推荐