@RefreshScope动态刷新配置和@value注入复杂对象

一、引入

在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源码了解更多细节。

相关推荐
BD_Marathon4 小时前
【Flink】部署模式
java·数据库·flink
鼠鼠我捏,要死了捏6 小时前
深入解析Java NIO多路复用原理与性能优化实践指南
java·性能优化·nio
ningqw6 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友7 小时前
vi编辑器命令常用操作整理(持续更新)
后端
superlls7 小时前
(Redis)主从哨兵模式与集群模式
java·开发语言·redis
胡gh7 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫8 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong8 小时前
技术人如何对客做好沟通(上篇)
后端
叫我阿柒啊9 小时前
Java全栈工程师面试实战:从基础到微服务的深度解析
java·redis·微服务·node.js·vue3·全栈开发·电商平台
颜如玉9 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源