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

相关推荐
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei2 小时前
java的类加载机制的学习
java·学习
码农小旋风3 小时前
详解K8S--声明式API
后端
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7894 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet