赞
踩
通常情况下我们@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
用来表示环境配置信息,这个接口有几个方法比较重要
String resolvePlaceholders(String text);
MutablePropertySources getPropertySources();
resolvePlaceholders用来解析${text}
的,@Value注解最后就是调用这个方法来解析的。
getPropertySources返回MutablePropertySources对象,来看一下这个类
public class MutablePropertySources implements PropertySources {
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
}
内部包含一个propertySourceList
列表。
spring容器中会有一个Environment
对象,最后会调用这个对象的resolvePlaceholders
方法解析@Value。
大家可以捋一下,最终解析@Value的过程:
1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
2. Environment内部会访问MutablePropertySources来解析
3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值
通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。
下面我们就按照这个思路来一个。
来个邮件配置信息类,内部使用@Value注入邮件配置信息
package com.javacode2018.lesson002.demo18.test2; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * 邮件配置信息 */ @Component public class MailConfig { @Value("${mail.host}") private String host; @Value("${mail.username}") private String username; @Value("${mail.password}") private String password; public String getHost() { return host; } public void setHost(String host) { this.host = host; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "MailConfig{" + "host='" + host + '\'' + ", username='" + username + '\'' + ", password='" + password + '\'' + '}'; } }
再来个类DbUtil
,getMailInfoFromDb
方法模拟从db中获取邮件配置信息,存放在map中
package com.javacode2018.lesson002.demo18.test2; import java.util.HashMap; import java.util.Map; public class DbUtil { /** * 模拟从db中获取邮件配置信息 * * @return */ public static Map<String, Object> getMailInfoFromDb() { Map<String, Object> result = new HashMap<>(); result.put("mail.host", "smtp.qq.com"); result.put("mail.username", "路人"); result.put("mail.password", "123"); return result; } }
来个spring配置类
package com.javacode2018.lesson002.demo18.test2;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class MainConfig2 {
}
下面是重点代码
@Test public void test2() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); /*下面这段是关键 start*/ //模拟从db中获取配置信息 Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb(); //将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类) MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb); //将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高 context.getEnvironment().getPropertySources().addFirst(mailPropertySource); /*上面这段是关键 end*/ context.register(MainConfig2.class); context.refresh(); MailConfig mailConfig = context.getBean(MailConfig.class); System.out.println(mailConfig); }
注释比较详细,就不详细解释了。
直接运行,看效果
MailConfig{host='smtp.qq.com', username='路人', password='123'}
有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb
,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。
上面重点是下面这段代码,大家需要理解
/*下面这段是关键 start*/
//模拟从db中获取配置信息
Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();
//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)
MapPropertySource mailPropertySource = new MapPropertySource("mail", mailInfoFromDb);
//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高
context.getEnvironment().getPropertySources().addFirst(mailPropertySource);
/*上面这段是关键 end*/
咱们继续看下一个问题
如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。
@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。
当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。
那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。
package com.javacode2018.lesson002.demo18.test4;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope(BeanRefreshScope.SCOPE_REFRESH)
@Documented
public @interface RefreshScope {
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@1
}
要求标注@RefreshScope注解的类支持动态刷新@Value的配置
@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS
下面类中有几个无关的方法去掉了,可以忽略
package com.javacode2018.lesson002.demo18.test4; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.Scope; import org.springframework.lang.Nullable; import java.util.concurrent.ConcurrentHashMap; 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; } }
上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中
上面的clean方法用来清理beanMap中当前已缓存的所有bean
package com.javacode2018.lesson002.demo18.test4; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * 邮件配置信息 */ @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方法,一会测试时候可以看效果。
package com.javacode2018.lesson002.demo18.test4; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MailService { @Autowired private MailConfig mailConfig; @Override public String toString() { return "MailService{" + "mailConfig=" + mailConfig + '}'; } }
代码比较简单,重写了toString方法,一会测试时候可以看效果。
package com.javacode2018.lesson002.demo18.test4; import java.util.HashMap; import java.util.Map; import java.util.UUID; 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; } }
package com.javacode2018.lesson002.demo18.test4;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class MainConfig4 {
}
内部有2个方法,如下:
package com.javacode2018.lesson002.demo18.test4; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.env.MapPropertySource; import java.util.Map; public class RefreshConfigUtil { /** * 模拟改变数据库中都配置信息 */ public static void updateDbConfig(AbstractApplicationContext context) { //更新context中的mailPropertySource配置信息 refreshMailPropertySource(context); //清空BeanRefreshScope中所有bean的缓存 BeanRefreshScope.getInstance().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.getInstance().clean()用来清除BeanRefreshScope中所有已经缓存的bean,那么调用bean的任意方法的时候,会重新出发spring容器来创建bean,spring容器重新创建bean的时候,会重新解析@Value的信息,此时容器中的邮件配置信息是新的,所以@Value注入的信息也是新的。
来个测试用例
@Test public void test4() throws InterruptedException { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.getBeanFactory().registerScope(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance()); context.register(MainConfig4.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); } }
@1:循环3次,输出mailService的信息
@2:循环3次,内部先通过@3来模拟更新db中配置信息,然后在输出mailService信息
配置未更新的情况下,输出3次
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
MailService{mailConfig=MailConfig{username='df321543-8ca7-4563-993a-bd64cbf50d53'}}
模拟3次更新配置效果
MailService{mailConfig=MailConfig{username='6bab8cea-9f4f-497d-a23a-92f15d0d6e34'}}
MailService{mailConfig=MailConfig{username='581bf395-f6b8-4b87-84e6-83d3c7342ca2'}}
MailService{mailConfig=MailConfig{username='db337f54-20b0-4726-9e55-328530af6999'}}
上面MailService输出了6次,前3次username的值都是一样的,后面3次username的值不一样了,说明修改配置起效了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。