一、引入
在springboot中,配置注入的一种方式是使用@value注解。当你需要注入的配置是json对象时,如下面定义的特权用户,数量很少,但是需要支持可配置。
css
[ { "userId": 11, "name": "Zhang san", "level": 1, "expiration": "2025-12-12 00:00:00" }, { "userId": 25, "name": "Li si", "level": 3, "expiration": "2024-05-31 00:00:00" }]
java
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.util.Date;
@Getter
@Setter
@NoArgsConstructor
public class PrivilegeUser {
private Long userId;
private String name;
/**
* 权限级别
*/
private Long level;
/**
* 过期时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date expiration;
}
二、@value注入复杂对象
2.1 直接注入List
此时,你可能会这样实现。在项目的application.properties中增加配置,并在业务逻辑中这样使用它。
properties
privilege.user.config=[{"userId":11,"name":"Zhang san","level":1,"expiration":"2025-12-12 00:00:00"},{"userId":25,"name":"Li si","level":3,"expiration":"2024-05-31 00:00:00"}]
java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class PrivilegeController {
@Value("${privilege.user.config}")
private List<PrivilegeUser> privilegeUserList;
@GetMapping("/has/privilege/{userId}")
public boolean hasPrivilege(@PathVariable Long userId) {
for (PrivilegeUser privilegeUser : privilegeUserList) {
if (privilegeUser.getUserId().equals(userId)) {
return true;
}
}
return false;
}
}
当你自信满满地启动项目时,发现启动报错了:不能将String转为PrivilegeUser: no matching editors or conversion strategy found.
2.2 每次使用都反序列化
既然配置是作为String被加载的,那我们用String接收就不会报错了。于是,你写下了这样的代码。
java
@Value("${privilege.user.config}")
private String privilegeUserConfig;
@GetMapping("/has/privilege/{userId}")
public boolean hasPrivilege(@PathVariable Long userId) {
// 使用时先反序列化
List<PrivilegeUser> privilegeUserList = Objects.isNull(privilegeUserConfig) ?
Collections.emptyList() : JSONArray.parseArray(privilegeUserConfig, PrivilegeUser.class);
// ......省略
return false;
}
能用吗?当然可以。只是每次使用该配置时,都得先反序列化,实在太不优雅了。
2.3 自定义类型转换器
起初启动报错,是因为找不到String到PrivilegeUser的转换方式聪明的你自然会想到能否自定义editors or conversion strategy
?果然,spring支持自定义类型转换器:实现Converter接口即可 。于是我们写出了下面的代码:
java
import com.alibaba.fastjson.JSONArray;
import org.springframework.core.convert.converter.Converter;
import java.util.List;
public class PrivilegeUserConverter implements Converter<String, List<PrivilegeUser>> {
@Override
public List<PrivilegeUser> convert(String config) {
return JSONArray.parseArray(config, PrivilegeUser.class);
}
}
java
// 在启动类中,注册自定义Converter
@Bean
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean factoryBean = new ConversionServiceFactoryBean();
factoryBean.setConverters(Collections.singleton(new PrivilegeUserConverter()));
return factoryBean;
}
此时,程序中就可以使用List接收配置了。
2.4 不自定义Converter
如果这种复杂类型的配置较多时,你不想定义一个个Converter,也可以这样实现:仍然用String接收配置,但是只需反序列化一次即可。
java
@Configuration
public class PrivilegeUserConfig implements InitializingBean {
@Value("${privilege.user.config:}")
private String privilegeUserConfig;
private List<PrivilegeUser> permissionList;
@Override
public void afterPropertiesSet() throws Exception {
permissionList = Objects.isNull(privilegeUserConfig) ?
Collections.emptyList() : JSONArray.parseArray(privilegeUserConfig, PrivilegeUser.class);
}
public List<PrivilegeUser> getPermissionList() {
return permissionList;
}
}
java
@Autowired
private PrivilegeUserConfig privilegeUserConfig;
@GetMapping("/has/privilege/{userId}")
public boolean hasPrivilege(@PathVariable Long userId) {
List<PrivilegeUser> privilegeUserList = privilegeUserConfig.getPrivilegeUserList();
// ......省略
return false;
}
当你使用配置中心如nacos等管理配置时,给PrivilegeUserConfig
类加上@Scope("refresh")
或@RefreshScope
,也能正常刷新配置。
接下来,我们看看@RefreshScope的大致实现。
三、@efreshScope实现配置动态刷新
其实,@RefreshScope是对@Scope("refresh")的封装。
3.1 缓存Scope Bean
类上标注了@RefreshScope
注解时,那么它的BeanDefinition信息中的scope=refresh,在getBean时不同于单例、多例bean,有自己的特殊逻辑。在org.springframework.beans.factory.support.AbstractBeanFactory
类中可看到。
java
protected <T> T doGetBean(
String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
// ......省略
try {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);
// 单例时
if (mbd.isSingleton()) {
// ......省略
} else if (mbd.isPrototype()) { // 多例bean时
// ......省略
} else {
// 如@Scope("refresh")会走到这
String scopeName = mbd.getScope();
if (!StringUtils.hasLength(scopeName)) {
throw new IllegalStateException("No scope name defined for bean ´" + beanName + "'");
}
Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
// 会走到GenericScope.get()
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
}
// ......省略
}
在org.springframework.cloud.context.scope.GenericScope
类中,get bean时先看缓存的bean是否为null,如果是则创建一个并缓存;如果不是则返回这个bean。
java
public Object get(String name, ObjectFactory<?> objectFactory) {
// 缓存起来
BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
try {
// BeanLifecycleWrapper.getBean()
return value.getBean();
} catch (RuntimeException exception) {
this.errors.put(name, exception);
throw exception;
}
}
BeanLifecycleWrapper是GenericScope的一个内部类。
kotlin
private static class BeanLifecycleWrapper {
// bean名称
private final String name;
private final ObjectFactory<?> objectFactory;
// bean实例
private Object bean;
private Runnable callback;
public Object getBean() {
// 为空时创建一个新的bean
if (this.bean == null) {
synchronized (this.name) {
if (this.bean == null) {
this.bean = this.objectFactory.getObject();
}
}
}
return this.bean;
}
public void destroy() {
if (this.callback != null) {
synchronized (this.name) {
Runnable callback = this.callback;
if (callback != null) {
callback.run();
}
// 清空引用
this.callback = null;
this.bean = null;
}
}
}
}
RefreshScope
继承了GenericScope,从下图中可以看到被缓存的bean。 从上面代码中可见,被@Scope
标注的类,创建的Bean会缓存到scope的cache中,getBean时先从缓存中获取,如果缓存中的bean为null, 则重新create bean
3.2 RefreshScope刷新bean缓存
在org.springframework.cloud.autoconfigure.RefreshAutoConfiguration中,创建了RefreshScope实例
当我们在nacos中进行配置更新发布后,将产生一个RefreshEvent
事件,RefreshEventListner
监听着该事件,对bean缓存进行了清空。
java
// RefreshEventListener类中
public void handle(RefreshEvent event) {
if (this.ready.get()) {
log.debug("Event received " + event.getEventDesc());
// refresh即ContextRefresher
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
然后走到org.springframework.cloud.context.refresh.ContextRefresher
。
java
public synchronized Set<String> refresh() {
Set<String> keys = this.refreshEnvironment();
// scope即RefreshScope实例
this.scope.refreshAll();
return keys;
}
调用到GenericScope中,将缓存的所有BeanLifecycleWrapper.bean赋值为null。
java
public void destroy() {
List<Throwable> errors = new ArrayList();
Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
Iterator iterator = wrappers.iterator();
// 遍历
while(iterator.hasNext()) {
BeanLifecycleWrapper wrapper = (BeanLifecycleWrapper)iterator.next();
try {
Lock lock = ((ReadWriteLock)this.locks.get(wrapper.getName())).writeLock();
lock.lock();
try {
// 将BeanLifecycleWrapper.bean赋值为null
wrapper.destroy();
} finally {
lock.unlock();
}
} catch (RuntimeException var10) {
errors.add(var10);
}
}
// ......省略
}
3.3 总结
只要配置中心进行了更新发布,就会导致scope=refresh的bean实例被赋值为null,在访问该bean时将由spring重新创建一个实例,那么新实例自然而然将持有最新的配置。
以上是对@RefreshScope
注解实现的大体介绍,感兴趣的小伙伴可以通过debug源码了解更多细节。