Spring进阶(十九)之配置动态刷新

目录

@Value的用法

@Value的使用步骤

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

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

@Value数据来源

案例:

@Value动态刷新

知识前沿

动态刷新@Value的实现

小结


@Value的用法

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

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

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

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

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

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

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

@Value的使用步骤

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

如:

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

语法:

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

如: 

  • @value("${username:sd}")   表示引用配置文件中username的值,如果不存在则默认值为sd
  • @value("${username}")     表示引用配置文件中的username的值,如果不存在则值为 ${username} 字符串

使用也比较简单,我们就不做案例了

@Value数据来源

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

spring中有个重要的类:

org.springframework.core.env.PropertySource

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

内部有个方法:

public abstract Object getProperty(String name);

 spring中还有一个比较重要的接口:

org.springframework.core.env.Environment

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

String resolvePlaceholders(String text);
MutablePropertySources getPropertySources();

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

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

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

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

大家再来捋一捋,上面@Value的解析过程:

1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析

2. Environment内部会访问MutablePropertySources来解析

3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用 PropertySource.getProperty方法来解析key对应的值

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

案例:

@Component
public class User {

    @Value("${user.username}")
    private String name;

    @Value("${user.password}")
    private String password;

    @Value("${user.age}")
    private Integer  age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", age=" + age +
                '}';
    }
}
@ComponentScan
@Configuration
public class UserConfig {

}
public class ValueTest {

    @Test
    public void test1(){

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

        //模拟db获取数据
        Map<String, Object> map = new HashMap<>();
        map.put("user.username","张三");
        map.put("user.password","123");
        map.put("user.age",18);
        //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
        MapPropertySource mapPropertySource = new MapPropertySource("user",map);
        //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
        context.getEnvironment().getPropertySources().addFirst(mapPropertySource);

        context.register(UserConfig.class);
        context.refresh();
        System.out.println(context.getBean("user"));
    }
}

运行输出:

User{name='张三', password='123', age=18}

上面的测试,我们并没有使用配置文件,而是模拟一db,同样能达到效果,所以,这意味着我们可以随便发挥,这数据可以来自mysql,redis等等,构造一个map出来,放到mapPropertySource中,然后添加进Environment 。

我们再来想一个问题:如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。 @Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了

@Value动态刷新

知识前沿

这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。 这个知识点是 自定义bean作用域 ,我们先来看一下@Scope这个注解的源码,有个参数是:

ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

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

public enum ScopedProxyMode {
    DEFAULT,
    NO,
    INTERFACES,
    TARGET_CLASS;
}

前面3个,不讲了,直接讲最后一个值是干什么的。当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象, 通过这个代理对象来访问目标bean对象。 理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Scope(BeanMyScope.SCOPE_MY)
public @interface MyScope {

    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

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

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

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

public class BeanMyScope implements Scope {

    public static final String SCOPE_MY = "my"; //@1

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        System.out.println("BeanMyScope >>>>>>>>> get:" + name); //@2
        return objectFactory.getObject(); //@3
    }

    @Override
    public Object remove(String name) {
        return null;
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {

    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return null;
    }
}

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

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

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

再来一个类,作用域为我们上面定义的作用域

@Component
@MyScope //@1
public class Car {

    private String name;

    public Car(){
        System.out.println("---------创建Car对象" + this); //@2
        this.name = UUID.randomUUID().toString(); //@3

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

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

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

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

再来一个配置类

@ComponentScan
@Configuration
public class CarConfig {

}

测试

    @Test
    public void test2(){

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        //将自定义作用域注册到spring容器中
        context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1
        context.register(CarConfig.class);
        context.refresh();
        System.out.println("从容器中获取Car对象");
        Car car = context.getBean(Car.class); //@2
        System.out.println("car对象的class为:" + car.getClass()); //@3
        System.out.println("多次调用car的getName感受一下效果\n");
        for (int i = 1; i <= 3; i++) {
            System.out.println(String.format("********\n第%d次开始调用getName", i));
            System.out.println(car.getName());
            System.out.println(String.format("第%d次调用getName结束\n********\n", i));
        }
    }

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

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

@3:输出这个bean对应的class,一会认真看一下,这个类型是不是Car类型的 代码后面又搞了3次循环,调用user的getName方法,并且方法前后分别输出了一行日志。

运行后输出:

从容器中获取Car对象
car对象的class为:class com.example.entity.Car$$EnhancerBySpringCGLIB$$870426db
多次调用car的getName感受一下效果

********
第1次开始调用getName
BeanMyScope >>>>>>>>> get:scopedTarget.car
---------创建Car对象com.example.entity.Car@35841320
47e35ef3-43b9-47fb-a700-bb56f5d67405
第1次调用getName结束
********

********
第2次开始调用getName
BeanMyScope >>>>>>>>> get:scopedTarget.car
---------创建Car对象com.example.entity.Car@480d3575
62e6063a-569a-4423-888c-5122d804aea4
第2次调用getName结束
********

********
第3次开始调用getName
BeanMyScope >>>>>>>>> get:scopedTarget.car
---------创建Car对象com.example.entity.Car@f1da57d
8c8cb556-3b53-4615-bf87-4e7e8a932957
第3次调用getName结束
********

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

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

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

后面的输出可以看出:

每次调用user.getName方法的时候,内部自动调用了 BeanMyScope#get 方法和 User的构造函数。

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

动态刷新@Value的实现

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

先来自定义一个Scope:RefreshScope

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope(BeanRefreshScope.SCOPE_REFRESH)
public @interface RefreshScope {
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@1
}

 要求标注@RefreshScope注解的类支持动态刷新@Value的配置 @1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS

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

public class BeanRefreshScope implements Scope {

    public static final String SCOPE_REFRESH = "refresh";

    private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();

    //来个map用来缓存bean
    private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1

    private BeanRefreshScope() {
    }

    public static BeanRefreshScope getInstance() {
        return INSTANCE;
    }

    /**
     * 清理当前
     */
    public static void clean() {
        INSTANCE.beanMap.clear();
    }

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get(name);
        if (bean == null) {
            bean = objectFactory.getObject();
            beanMap.put(name, bean);
        }
        return bean;
    }


    @Override
    public Object remove(String name) {
        return null;
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {

    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return null;
    }
}

上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建 bean的实例,然后丢到beanMap中 上面的clean方法用来清理beanMap中当前已缓存的所有bean

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

/**
 * 邮件配置信息
 */
@Component
@RefreshScope //@1
public class MailConfig {

    @Value("${mail.username}") //@2
    private String username;

    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    @Override
    public String toString() {
        return "MailConfig{" +
                "username='" + username + '\'' +
                '}';
    }
}

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

@2:通过@Value注入mail.username对一个的值 重写了toString方法,一会测试时候可以看效果。

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

@Component
public class MailService {

    @Autowired
    private MailConfig mailConfig;

    @Override
    public String toString() {
        return "MailService{" +
                "mailConfig=" + mailConfig +
                '}';
    }
}

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

public class DbUtil {
    /**
     * 模拟从db中获取邮件配置信息
     *
     * @return
     */
    public static Map<String, Object> getMailInfoFromDb() {
        Map<String, Object> result = new HashMap<>();
        result.put("mail.username", UUID.randomUUID().toString());
        return result;
    }
}

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

@Configuration
@ComponentScan
public class MainConfig {
}

来个工具类

public class RefreshConfigUtil {
    /**
     * 模拟改变数据库中都配置信息
     */
    public static void updateDbConfig(AbstractApplicationContext context) {

        //更新context中的mailPropertySource配置信息
        refreshMailPropertySource(context);
        //清空BeanRefreshScope中所有bean的缓存
        BeanRefreshScope.clean();
    }

    
    public static void refreshMailPropertySource(AbstractApplicationContext context) {
        Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
        //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
        MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
        context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
    }
}

updateDbConfig方法模拟修改db中配置的时候需要调用的方法,方法中2行代码,第一行代码调 用refreshMailPropertySource方法修改容器中邮件的配置信息 BeanRefreshScope.clean()用来清除BeanRefreshScope中所有已经缓存的bean, 那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean 的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。

测试

    @Test
    public void test3()throws Exception{

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());
        context.register(MainConfig.class);
        //刷新mail的配置到Environment
        RefreshConfigUtil.refreshMailPropertySource(context);
        context.refresh();
        MailService mailService = context.getBean(MailService.class);
        System.out.println("配置未更新的情况下,输出3次");
        for (int i = 0; i < 3; i++) { //@1
            System.out.println(mailService);
            TimeUnit.MILLISECONDS.sleep(200);
        }
        System.out.println("模拟3次更新配置效果");
        for (int i = 0; i < 3; i++) { //@2
            RefreshConfigUtil.updateDbConfig(context); //@3
            System.out.println(mailService);
            TimeUnit.MILLISECONDS.sleep(200);
        }
    }

运行输出:

配置未更新的情况下,输出3次
MailService{mailConfig=MailConfig{username='a3ada8d1-d5c3-4c72-ac61-ec48186d55a0'}}
MailService{mailConfig=MailConfig{username='a3ada8d1-d5c3-4c72-ac61-ec48186d55a0'}}
MailService{mailConfig=MailConfig{username='a3ada8d1-d5c3-4c72-ac61-ec48186d55a0'}}


模拟3次更新配置效果
MailService{mailConfig=MailConfig{username='21dfbda7-a0ab-45f3-a45f-10ae39e431ef'}}
MailService{mailConfig=MailConfig{username='203af863-6121-495d-a5c8-a4e50fd79839'}}
MailService{mailConfig=MailConfig{username='f4a0383c-d96c-45c6-9f7c-a151a296b4c4'}}

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

小结

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

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

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Boot中,我们可以通过`ConfigurableEnvironment`接口来动态修改配置项的值。以下是一种通过代码修改`application.yml`配置文件中某个配置项的值的方法: 1. 首先,引入`org.springframework.core.env.ConfigurableEnvironment`类。 ```java import org.springframework.core.env.ConfigurableEnvironment; ``` 2. 在需要修改配置项的地方,注入`ConfigurableEnvironment`对象。 ```java @Autowired private ConfigurableEnvironment environment; ``` 3. 使用`getProperty`方法获取要修改的配置项的值。 ```java String value = environment.getProperty("your.property.key"); ``` 4. 修改值后,使用`getPropertySources`方法获取配置项的集合。 ```java MutablePropertySources propertySources = environment.getPropertySources(); ``` 5. 通过`get`方法获取`PropertySource`对象。 ```java PropertySource source = propertySources.get("applicationConfig: [classpath:/application.yml]"); ``` 6. 使用`getProperty`方法获取要修改的配置项的值。 ```java String value = (String) source.getProperty("your.property.key"); ``` 7. 调用`setProperty`方法修改配置项的值。 ```java source.setProperty("your.property.key", "new value"); ``` 8. 重新加载配置项。 ```java environment.reload(); ``` 9. 新的配置项值已经生效。 这种方式适用于需要在运行时动态修改配置的情况,但需要注意,修改的配置项仅在当前应用程序运行时有效,不会写回到`application.yml`文件中。如果需要将修改写回配置文件,可以通过其他方式实现,例如使用`java.nio`包中的API来修改配置文件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰魄雕狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值