当前位置:   article > 正文

Spring成神之路第二十五篇:@Value【用法、数据来源、动态刷新】_@value动态配置

@value动态配置

面试官:Spring中的@Value用过么,介绍一下

我:@Value可以标注在字段上面,可以将外部配置文件中的数据,比如可以将数据库的一些配置信息放在配置文件中,然后通过@Value的方式将其注入到bean的一些字段中

面试官:那就是说@Value的数据来源于配置文件了?

我:嗯,我们项目最常用更多就是通过@Value来引用Properties文件中的配置

面试官:@Value数据来源还有其他方式么?

我:此时我异常开心,刚好问的我都研究过,我说:当然有,可以将配置信息放在db或者其他存储介质中,容器启动的时候,可以将这些信息加载到Environment中,@Value中应用的值最终是通过Environment来解析的,所以只需要扩展一下Environment就可以实现了。

面试官:不错嘛,看来你对spring研究的还是可以,是不是喜欢研究spring源码?

我:笑着说,嗯,平时有空的时候确实喜欢捣鼓捣鼓源码,感觉自己对spring了解的还可以,不能算精通,也算是半精通吧

面试官:看着我笑了笑,那@Value的注入的值可以动态刷新么?

我:应该可以吧,我记得springboot中有个@RefreshScope注解就可以实现你说的这个功能

面试官:那你可以说一下@RefreshScope是如何实现的么,可以大概介绍一下?

我:嗯。。。这个之前看过一点,不过没有看懂

面试官:没关系,你可以回去了再研究一下;你期望工资多少?

我:3万吧

面试官:今天的面试还算是可以的,不过如果@RefreshScope能回答上来就更好了,这块是个加分项,不过也确实有点难度,2.5万如何?

我:(心中默默想了想:2.5万,就是一个问题没有回答好,砍了5000,有点狠啊,我要回去再研究研究,3万肯定是没问题的),我说:最低2.9万

面试官:那谢谢你,今天面试就到这里,出门右拐,不送!

我有个好习惯,每次面试回去之后,都会进行复盘,把没有搞定的问题一定要想办法搞定,这样才不虚。

这次面试问题如下

  1. @Value的用法

  2. @Value数据来源

  3. @Value动态刷新的问题

@Value的用法

系统中需要连接db,连接db有很多配置信息。

系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。

还有其他的一些配置信息。

我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。

那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题

通常我们会将配置信息以key=value的形式存储在properties配置文件中。

通过@Value("${配置文件中的key}")来引用指定的key对应的value。

@Value使用步骤

步骤一:使用@PropertySource注解引入配置文件

将@PropertySource放在类上面,如下

@PropertySource({"配置文件路径1","配置文件路径2"...})

@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。

如:

  1. @Component
  2. @PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
  3. public class DbConfig {
  4. }

步骤二:使用@Value注解引用配置文件的值

通过@Value引用上面配置文件中的值:

语法

  1. @Value("${配置文件中的key:默认值}")
  2. @Value("${配置文件中的key}")

如:

@Value("${password:123}")

上面如果password不存在,将123作为值

@Value("${password}")

上面如果password不存在,值为${password}

假如配置文件如下

  1. jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
  2. jdbc.username=javacode
  3. jdbc.password=javacode

使用方式如下:

  1. @Value("${jdbc.url}")
  2. private String url;
  3. @Value("${jdbc.username}")
  4. private String username;
  5. @Value("${jdbc.password}")
  6. private String password;

下面来看案例

案例

来个配置文件db.properties

  1. jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8
  2. jdbc.username=javacode
  3. jdbc.password=javacode

来个配置类,使用@PropertySource引入上面的配置文件

  1. package com.javacode2018.lesson002.demo18.test1;
  2. import org.springframework.beans.factory.annotation.Configurable;
  3. import org.springframework.context.annotation.ComponentScan;
  4. import org.springframework.context.annotation.PropertySource;
  5. @Configurable
  6. @ComponentScan
  7. @PropertySource({"classpath:com/javacode2018/lesson002/demo18/db.properties"})
  8. public class MainConfig1 {
  9. }

来个类,使用@Value来使用配置文件中的信息

  1. package com.javacode2018.lesson002.demo18.test1;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.stereotype.Component;
  4. @Component
  5. public class DbConfig {
  6.     @Value("${jdbc.url}")
  7.     private String url;
  8.     @Value("${jdbc.username}")
  9.     private String username;
  10.     @Value("${jdbc.password}")
  11.     private String password;
  12.     public String getUrl() {
  13.         return url;
  14.     }
  15.     public void setUrl(String url) {
  16.         this.url = url;
  17.     }
  18.     public String getUsername() {
  19.         return username;
  20.     }
  21.     public void setUsername(String username) {
  22.         this.username = username;
  23.     }
  24.     public String getPassword() {
  25.         return password;
  26.     }
  27.     public void setPassword(String password) {
  28.         this.password = password;
  29.     }
  30.     @Override
  31.     public String toString() {
  32.         return "DbConfig{" +
  33.                 "url='" + url + '\'' +
  34.                 ", username='" + username + '\'' +
  35.                 ", password='" + password + '\'' +
  36.                 '}';
  37.     }
  38. }

上面重点在于注解@Value注解,注意@Value注解中的

来个测试用例

  1. package com.javacode2018.lesson002.demo18;
  2. import com.javacode2018.lesson002.demo18.test1.DbConfig;
  3. import com.javacode2018.lesson002.demo18.test1.MainConfig1;
  4. import org.junit.Test;
  5. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  6. public class ValueTest {
  7.     @Test
  8.     public void test1() {
  9.         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  10.         context.register(MainConfig1.class);
  11.         context.refresh();
  12.         DbConfig dbConfig = context.getBean(DbConfig.class);
  13.         System.out.println(dbConfig);
  14.     }
  15. }

运行输出

DbConfig{url='jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8', username='javacode', password='javacode'}

上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。

@Value数据来源

通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。

我们需要先了解一下@Value中数据来源于spring的什么地方。

spring中有个类

org.springframework.core.env.PropertySource

可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息

内部有个方法:

public abstract Object getProperty(String name);

通过name获取对应的配置信息。

系统有个比较重要的接口

org.springframework.core.env.Environment

用来表示环境配置信息,这个接口有几个方法比较重要

  1. String resolvePlaceholders(String text);
  2. MutablePropertySources getPropertySources();

resolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。

getPropertySources返回MutablePropertySources对象,来看一下这个类

  1. public class MutablePropertySources implements PropertySources {
  2.     private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
  3. }

内部包含一个propertySourceList列表。

spring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。

大家可以捋一下,最终解析@Value的过程:

  1. 1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
  2. 2Environment内部会访问MutablePropertySources来解析
  3. 3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值

通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。

下面我们就按照这个思路来一个。

来个邮件配置信息类,内部使用@Value注入邮件配置信息

  1. package com.javacode2018.lesson002.demo18.test2;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.stereotype.Component;
  4. /**
  5.  * 邮件配置信息
  6.  */
  7. @Component
  8. public class MailConfig {
  9.     @Value("${mail.host}")
  10.     private String host;
  11.     @Value("${mail.username}")
  12.     private String username;
  13.     @Value("${mail.password}")
  14.     private String password;
  15.     public String getHost() {
  16.         return host;
  17.     }
  18.     public void setHost(String host) {
  19.         this.host = host;
  20.     }
  21.     public String getUsername() {
  22.         return username;
  23.     }
  24.     public void setUsername(String username) {
  25.         this.username = username;
  26.     }
  27.     public String getPassword() {
  28.         return password;
  29.     }
  30.     public void setPassword(String password) {
  31.         this.password = password;
  32.     }
  33.     @Override
  34.     public String toString() {
  35.         return "MailConfig{" +
  36.                 "host='" + host + '\'' +
  37.                 ", username='" + username + '\'' +
  38.                 ", password='" + password + '\'' +
  39.                 '}';
  40.     }
  41. }

再来个类DbUtilgetMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中

  1. package com.javacode2018.lesson002.demo18.test2;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. public class DbUtil {
  5.     /**
  6.      * 模拟从db中获取邮件配置信息
  7.      *
  8.      * @return
  9.      */
  10.     public static Map<StringObject> getMailInfoFromDb() {
  11.         Map<StringObject> result = new HashMap<>();
  12.         result.put("mail.host""smtp.qq.com");
  13.         result.put("mail.username""路人");
  14.         result.put("mail.password""123");
  15.         return result;
  16.     }
  17. }

来个spring配置类

  1. package com.javacode2018.lesson002.demo18.test2;
  2. import org.springframework.context.annotation.ComponentScan;
  3. import org.springframework.context.annotation.Configuration;
  4. @Configuration
  5. @ComponentScan
  6. public class MainConfig2 {
  7. }

下面是重点代码

  1. @Test
  2. public void test2() {
  3.     AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  4.     /*下面这段是关键 start*/
  5.     //模拟从db中获取配置信息
  6.     Map<StringObject> mailInfoFromDb = DbUtil.getMailInfoFromDb();
  7.     //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
  8.     MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
  9.     //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
  10.     context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
  11.     /*上面这段是关键 end*/
  12.     context.register(MainConfig2.class);
  13.     context.refresh();
  14.     MailConfig mailConfig = context.getBean(MailConfig.class);
  15.     System.out.println(mailConfig);
  16. }

注释比较详细,就不详细解释了。

直接运行,看效果

MailConfig{host='smtp.qq.com', username='路人', password='123'}

有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。

上面重点是下面这段代码,大家需要理解

  1. /*下面这段是关键 start*/
  2. //模拟从db中获取配置信息
  3. Map<StringObject> mailInfoFromDb = DbUtil.getMailInfoFromDb();
  4. //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
  5. MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
  6. //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
  7. context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
  8. /*上面这段是关键 end*/

咱们继续看下一个问题

如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。

@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。

实现@Value动态刷新

先了解一个知识点

这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。

这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解

bean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:

ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中

  1. public enum ScopedProxyMode {
  2.     DEFAULT,
  3.     NO,
  4.     INTERFACES,
  5.     TARGET_CLASS;
  6. }

前面3个,不讲了,直接讲最后一个值是干什么的。

当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。

理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。

自定义一个bean作用域的注解

  1. package com.javacode2018.lesson002.demo18.test3;
  2. import org.springframework.context.annotation.Scope;
  3. import org.springframework.context.annotation.ScopedProxyMode;
  4. import java.lang.annotation.*;
  5. @Target({ElementType.TYPE, ElementType.METHOD})
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @Documented
  8. @Scope(BeanMyScope.SCOPE_MY) //@1
  9. public @interface MyScope {
  10.     /**
  11.      * @see Scope#proxyMode()
  12.      */
  13.     ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2
  14. }

@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。

@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS

@MyScope注解对应的Scope实现如下

  1. package com.javacode2018.lesson002.demo18.test3;
  2. import org.springframework.beans.factory.ObjectFactory;
  3. import org.springframework.beans.factory.config.Scope;
  4. import org.springframework.lang.Nullable;
  5. /**
  6.  * @see MyScope 作用域的实现
  7.  */
  8. public class BeanMyScope implements Scope {
  9.     public static final String SCOPE_MY = "my"//@1
  10.     @Override
  11.     public Object get(String name, ObjectFactory<?> objectFactory) { 
  12.         System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2
  13.         return objectFactory.getObject(); //@3
  14.     }
  15.     @Nullable
  16.     @Override
  17.     public Object remove(String name) {
  18.         return null;
  19.     }
  20.     @Override
  21.     public void registerDestructionCallback(String name, Runnable callback) {
  22.     }
  23.     @Nullable
  24.     @Override
  25.     public Object resolveContextualObject(String key) {
  26.         return null;
  27.     }
  28.     @Nullable
  29.     @Override
  30.     public String getConversationId() {
  31.         return null;
  32.     }
  33. }

@1:定义了一个常量,作为作用域的值

@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果

@3:通过objectFactory.getObject()获取bean实例返回。

下面来创建个类,作用域为上面自定义的作用域

  1. package com.javacode2018.lesson002.demo18.test3;
  2. import org.springframework.stereotype.Component;
  3. import java.util.UUID;
  4. @Component
  5. @MyScope //@1 
  6. public class User {
  7.     private String username;
  8.     public User() { 
  9.         System.out.println("---------创建User对象" + this); //@2
  10.         this.username = UUID.randomUUID().toString(); //@3
  11.     }
  12.     public String getUsername() {
  13.         return username;
  14.     }
  15.     public void setUsername(String username) {
  16.         this.username = username;
  17.     }
  18. }

@1:使用了自定义的作用域@MyScope

@2:构造函数中输出一行日志

@3:给username赋值,通过uuid随机生成了一个

来个spring配置类,加载上面@Compontent标注的组件

  1. package com.javacode2018.lesson002.demo18.test3;
  2. import org.springframework.context.annotation.ComponentScan;
  3. import org.springframework.context.annotation.Configuration;
  4. @ComponentScan
  5. @Configuration
  6. public class MainConfig3 {
  7. }

下面重点来了,测试用例

  1. @Test
  2. public void test3() throws InterruptedException {
  3.     AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  4.     //将自定义作用域注册到spring容器中
  5.     context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1
  6.     context.register(MainConfig3.class);
  7.     context.refresh();
  8.     System.out.println("从容器中获取User对象");
  9.     User user = context.getBean(User.class); //@2
  10.     System.out.println("user对象的class为:" + user.getClass()); //@3
  11.     System.out.println("多次调用user的getUsername感受一下效果\n");
  12.     for (int i = 1; i <= 3; i++) {
  13.         System.out.println(String.format("********\n第%d次开始调用getUsername", i));
  14.         System.out.println(user.getUsername());
  15.         System.out.println(String.format("第%d次调用getUsername结束\n********\n", i));
  16.     }
  17. }

@1:将自定义作用域注册到spring容器中

@2:从容器中获取User对应的bean

@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的

代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。

见证奇迹的时候到了,运行输出

  1. 从容器中获取User对象
  2. user对象的class为:class com.javacode2018.lesson002.demo18.test3.User$$EnhancerBySpringCGLIB$$80233127
  3. 多次调用user的getUsername感受一下效果
  4. ********
  5. 1次开始调用getUsername
  6. BeanMyScope >>>>>>>>> get:scopedTarget.user
  7. ---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f4
  8. 7b41aa80-7569-4072-9d40-ec9bfb92f438
  9. 1次调用getUsername结束
  10. ********
  11. ********
  12. 2次开始调用getUsername
  13. BeanMyScope >>>>>>>>> get:scopedTarget.user
  14. ---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b
  15. 01d67154-95f6-44bb-93ab-05a34abdf51f
  16. 2次调用getUsername结束
  17. ********
  18. ********
  19. 3次开始调用getUsername
  20. BeanMyScope >>>>>>>>> get:scopedTarget.user
  21. ---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d15
  22. 76d0e86f-8331-4303-aac7-4acce0b258b8
  23. 3次调用getUsername结束
  24. ********

从输出的前2行可以看出:

  1. 调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象

  2. 第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。

后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。

通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。

动态刷新@Value具体实现

那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。

先来自定义一个Scope:RefreshScope

  1. package com.javacode2018.lesson002.demo18.test4;
  2. import org.springframework.context.annotation.Scope;
  3. import org.springframework.context.annotation.ScopedProxyMode;
  4. import java.lang.annotation.*;
  5. @Target({ElementType.TYPE, ElementType.METHOD})
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @Scope(BeanRefreshScope.SCOPE_REFRESH)
  8. @Documented
  9. public @interface RefreshScope {
  10.     ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@1
  11. }

要求标注@RefreshScope注解的类支持动态刷新@Value的配置

@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS

这个自定义Scope对应的解析类

下面类中有几个无关的方法去掉了,可以忽略

  1. package com.javacode2018.lesson002.demo18.test4;
  2. import org.springframework.beans.factory.ObjectFactory;
  3. import org.springframework.beans.factory.config.Scope;
  4. import org.springframework.lang.Nullable;
  5. import java.util.concurrent.ConcurrentHashMap;
  6. public class BeanRefreshScope implements Scope {
  7.     public static final String SCOPE_REFRESH = "refresh";
  8.     private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();
  9.     //来个map用来缓存bean
  10.     private ConcurrentHashMap<StringObject> beanMap = new ConcurrentHashMap<>(); //@1
  11.     private BeanRefreshScope() {
  12.     }
  13.     public static BeanRefreshScope getInstance() {
  14.         return INSTANCE;
  15.     }
  16.     /**
  17.      * 清理当前
  18.      */
  19.     public static void clean() {
  20.         INSTANCE.beanMap.clear();
  21.     }
  22.     @Override
  23.     public Object get(String name, ObjectFactory<?> objectFactory) {
  24.         Object bean = beanMap.get(name);
  25.         if (bean == null) {
  26.             bean = objectFactory.getObject();
  27.             beanMap.put(name, bean);
  28.         }
  29.         return bean;
  30.     }
  31. }

上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中

上面的clean方法用来清理beanMap中当前已缓存的所有bean

来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope

  1. package com.javacode2018.lesson002.demo18.test4;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.stereotype.Component;
  4. /**
  5.  * 邮件配置信息
  6.  */
  7. @Component
  8. @RefreshScope //@1
  9. public class MailConfig {
  10.     @Value("${mail.username}"//@2
  11.     private String username;
  12.     public String getUsername() {
  13.         return username;
  14.     }
  15.     public void setUsername(String username) {
  16.         this.username = username;
  17.     }
  18.     @Override
  19.     public String toString() {
  20.         return "MailConfig{" +
  21.                 "username='" + username + '\'' +
  22.                 '}';
  23.     }
  24. }

@1:使用了自定义的作用域@RefreshScope

@2:通过@Value注入mail.username对一个的值

重写了toString方法,一会测试时候可以看效果。

再来个普通的bean,内部会注入MailConfig

  1. package com.javacode2018.lesson002.demo18.test4;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.stereotype.Component;
  4. @Component
  5. public class MailService {
  6.     @Autowired
  7.     private MailConfig mailConfig;
  8.     @Override
  9.     public String toString() {
  10.         return "MailService{" +
  11.                 "mailConfig=" + mailConfig +
  12.                 '}';
  13.     }
  14. }

代码比较简单,重写了toString方法,一会测试时候可以看效果。

来个类,用来从db中获取邮件配置信息

  1. package com.javacode2018.lesson002.demo18.test4;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. import java.util.UUID;
  5. public class DbUtil {
  6.     /**
  7.      * 模拟从db中获取邮件配置信息
  8.      *
  9.      * @return
  10.      */
  11.     public static Map<StringObject> getMailInfoFromDb() {
  12.         Map<StringObject> result = new HashMap<>();
  13.         result.put("mail.username", UUID.randomUUID().toString());
  14.         return result;
  15.     }
  16. }

来个spring配置类,扫描加载上面的组件

  1. package com.javacode2018.lesson002.demo18.test4;
  2. import org.springframework.context.annotation.ComponentScan;
  3. import org.springframework.context.annotation.Configuration;
  4. @Configuration
  5. @ComponentScan
  6. public class MainConfig4 {
  7. }

来个工具类

内部有2个方法,如下:

  1. package com.javacode2018.lesson002.demo18.test4;
  2. import org.springframework.context.support.AbstractApplicationContext;
  3. import org.springframework.core.env.MapPropertySource;
  4. import java.util.Map;
  5. public class RefreshConfigUtil {
  6.     /**
  7.      * 模拟改变数据库中都配置信息
  8.      */
  9.     public static void updateDbConfig(AbstractApplicationContext context) {
  10.         //更新context中的mailPropertySource配置信息
  11.         refreshMailPropertySource(context);
  12.         //清空BeanRefreshScope中所有bean的缓存
  13.         BeanRefreshScope.getInstance().clean();
  14.     }
  15.     public static void refreshMailPropertySource(AbstractApplicationContext context) {
  16.         Map<StringObject> mailInfoFromDb = DbUtil.getMailInfoFromDb();
  17.         //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
  18.         MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
  19.         context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
  20.     }
  21. }

updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调用refreshMailPropertySource方法修改容器中邮件的配置信息

BeanRefreshScope.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。

来个测试用例

  1. @Test
  2. public void test4() throws InterruptedException {
  3.     AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
  4.     context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());
  5.     context.register(MainConfig4.class);
  6.     //刷新mail的配置到Environment
  7.     RefreshConfigUtil.refreshMailPropertySource(context);
  8.     context.refresh();
  9.     MailService mailService = context.getBean(MailService.class);
  10.     System.out.println("配置未更新的情况下,输出3次");
  11.     for (int i = 0; i < 3; i++) { //@1
  12.         System.out.println(mailService);
  13.         TimeUnit.MILLISECONDS.sleep(200);
  14.     }
  15.     System.out.println("模拟3次更新配置效果");
  16.     for (int i = 0; i < 3; i++) { //@2
  17.         RefreshConfigUtil.updateDbConfig(context); //@3
  18.         System.out.println(mailService);
  19.         TimeUnit.MILLISECONDS.sleep(200);
  20.     }
  21. }

@1:循环3次,输出mailService的信息

@2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息

见证奇迹的时刻,来看效果

  1. 配置未更新的情况下,输出3
  2. MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
  3. MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
  4. MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
  5. 模拟3次更新配置效果
  6. MailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}
  7. MailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}
  8. MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}

上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。

小结

动态@Value实现的关键是@Scope中proxyMode参数,值为ScopedProxyMode.DEFAULT,会生成一个代理,通过这个代理来实现@Value动态刷新的效果,这个地方是关键。

有兴趣的可以去看一下springboot中的@RefreshScope注解源码,和我们上面自定义的@RefreshScope类似,实现原理类似的。

总结

本次面试过程中3个问题,我们都搞定了,希望你也已经掌握了,有问题的欢迎给我留言,交流!

案例源码

https://gitee.com/javacode2018/spring-series

路人甲java所有案例代码以后都会放到这个上面,大家watch一下,可以持续关注动态。

来源:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648934401&idx=1&sn=98e726ec9adda6d40663f624705ba2e4&chksm=8862103fbf15992981183abef03b4774ab1dfd990a203a183efb8d118455ee4b477dc6cba50d&token=636643900&lang=zh_CN&scene=21#wechat_redirect

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/496063
推荐阅读
相关标签
  

闽ICP备14008679号