自己动手实现springboot配置(非)中心
好久没写博客了,这段时间主要是各种充电,因为前面写的一些东西,可能大家不太感兴趣或者是嫌弃没啥技术含量,所以这次特意下了一番功夫。这篇博客其实我花了周末整整两天写好了第一个版本,已经开源出去了,同样是像以前那样用来抛砖引玉。下面进入正题!
当我们想在springboot实现一个配置集中管理,自动更新就会遇到如下尴尬的场景:
1. 啥?我就存个配置还要安装个配置中心服务,配置中心服务挂了咋办,你给我重启吗?
2. 啥?配置中心也要高可用,还要部署多个避免单点故障,服务器资源不要钱吗,我分分钟能有个小目标吗?
3. 啥?你跟我说我存个配置还要有个单独的地方存储,什么git,阿波罗,git还用过,阿波罗?我是要登月吗?
4. 啥?我实现一个在线更新配置还要依赖actuator模块,这是个什么东西
5. 啥?我还要依赖消息队列,表示没用过
6. 啥?还要引入springcloud bus,啥子鬼东西,压根不知道你说啥
我想大多人遇到上面那些场景,都会对配置中心望而却步吧,实在是太麻烦了。我就想实现一个可以自动更新配置的功能就要安装一个单独的服务,还要考虑单独服务都应该考虑的各种问题,负载均衡,高可用,唉!这东西不是人能用的,已经在用的哥们姐们,你们都是神!很反感一想到微服务就要部署一大堆依赖服务,什么注册中心,服务网关,消息队列我也就忍了,你一个放配置的也要来个配置中心,还要部署多个来个高可用,你丫的不要跟我说部属一个单点就行了,你牛,你永远不会挂!所以没足够服务器不要想着玩太多花,每个java服务就要用一个单独的虚拟机加载全套的jar包(这里说的是用的最多的jdk8,据说后面版本可以做到公用一部分公共的jar),这都要资源。投机取巧都是我这种懒得学习这些新技术新花样的人想出来的。下面开始我们自己实现一个可以很方便的嵌入到自己的springboot项目中而不需要引入新服务的功能。
想到要实现一个外部公共地方存放配置,首先可以想到把配置存在本地磁盘或者网络,我们先以本地磁盘为例进行今天的分享。要实现一个在运行时随时修改配置的功能需要解决如下问题:
1. 怎么让服务启动就读取自己需要让他读取的配置文件(本地磁盘的,网络的,数据库里的配置)
2. 怎么随时修改如上的配置文件,并且同时刷新spring容器中的配置(热更新配置)
3. 怎么把功能集成到自己的springboot项目中
要实现第一点很简单,如果是本地文件系统,java nio有一个文件监听的功能,可以监听一个指定的文件夹,文件夹里的文件修改都会已事件的方式发出通知,按照指定方式实现即可。要实现第二点就有点困难了,首先要有一个共识,spring中的bean都会在启动阶段被封装成BeanDefinition对象放在map中,这些BeanDefinition对象可以类比java里每个类都会有一个Class对象模板,后续生成的对象都是以Class对象为模板生成的。spring中国同样也是以BeanDefinition为模板生成对象的,所以基本要用到的所有信息在BeanDefinition都能找到。由于我们项目中绝大多数被spring管理的对象都是单例的,没人会恶心到把配置类那些都搞成多例的吧!既然是单例我们只要从spring容器中找到,再通过反射强行修改里面的@Value修饰的属性不就行了,如果你们以为就这么简单,那就没有今天这篇博客了。如下:
复制代码
private void updateValue(Map props) {
Map classMap = applicationContext.getBeansWithAnnotation(RefreshScope.class);
if(classMap == null || classMap.isEmpty()) {
return;
}
classMap.forEach((beanName,bean) -> {
/**
* 这是一个坑爹的东西,这里保存一下spring生成的代理类的字节码由于有些@Value可能在@Configuration修饰的配置类下,
* 被这个注解修饰的配置类里面的属性在代理类会消失,只留下对应的getXX和setXX方法,导致下面不能直接通过反射直接
* 修改属性的值,只能通过反射调用对应setXX方法修改属性的值
*/
// saveProxyClass(bean);
Class> clazz = bean.getClass();
/**
* 获取所有可用的属性
*/
Field[] fields = clazz.getDeclaredFields();
/**
* 使用反射直接根据属性修改属性值
*/
setValue(bean,fields,props);
});
}
private void setValue(Object bean,Field[] fields,Map props) {
for(Field field : fields) {
Value valueAnn = field.getAnnotation(Value.class);
if (valueAnn == null) {
continue;
}
String key = valueAnn.value();
if (key == null) {
continue;
}
key = key.replaceAll(VALUE_REGEX,"$1");
key = key.split(COLON)[0];
if (props.containsKey(key)) {
field.setAccessible(true);
try {
field.set(bean, props.get(key));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* 只为测试导出代理对象然后反编译
* @param bean
*/
private void saveProxyClass(Object bean) {
byte[] bytes = ProxyGenerator.generateProxyClass("T", new Class[]{bean.getClass()});
try {
Files.write(Paths.get("F:\\fail2\\","T.class"),bytes, StandardOpenOption.CREATE);
} catch (IOException e) {
e.printStackTrace();
}
}
复制代码
如上代码,完全使用反射直接强行修改属性值,确实可以解决一部分属性修改的问题,但是还有一部分被@Configuration修饰的类就做不到了,因为spring中使用cglib对修饰这个注解的类做了代理,其实可以理解成生成了另外一个完全不一样的类,类里那些被@Value修饰的属性都被去掉了,就留下一堆setXX方法,鬼知道那些方法要使用那些key去注入配置。如果有人说使用上面代码将就下把需要修改的配置不放在被@Configuration修饰的类下就好了。如果有这么low,我就不用写这篇博客了,要实现就实现一个五脏俱全的功能,不能让使用者迁就你的不足。这里提一下上面的@RefreshScope注解,这个注解并不是springboot中的配置服务那个注解,是自己定义的一个同名注解,因为用它那个要引入配置服务的依赖,为了一个注解引入一个依赖不值得。下面是实现配置更新的核心类:
复制代码
package com.rdpaas.easyconfig.context;
import com.rdpaas.easyconfig.ann.RefreshScope;
import com.rdpaas.easyconfig.observer.ObserverType;
import com.rdpaas.easyconfig.observer.Observers;
import com.rdpaas.easyconfig.utils.PropUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* 自定义的springboot上下文类
* @author rongdi
* @date 2019-09-21 10:30:01
*/
public class SpringBootContext implements ApplicationContextAware {
private Logger logger = LoggerFactory.getLogger(SpringBootContext.class);
private final static String REFRESH_SCOPE_ANNOTATION_NAME = "com.rdpaas.easyconfig.ann.RefreshScope";
private final static Map, SpringAnnotatedRefreshScopeBeanInvoker> refreshScopeBeanInvokorMap = new HashMap<>();
private final static String COLON = ":";
private final static String SET_PREFIX = "set";
private final static String VALUE_REGEX = "\\$\\{(.*)}";
private static ApplicationContext applicationContext;
private static Environment environment;
private static String filePath;
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@Override
public void setApplicationContext(ApplicationContext ac) throws BeansException {
applicationContext = ac;
try {
/**
* 初始化准备好哪些类需要更新配置,放入map
*/
init();
/**
* 如果有配置文件中配置了文件路径,并且是本地文件,则开启对应位置的文件监听
*/
if(filePath != null && !PropUtil.isWebProp(filePath)) {
File file = new File(filePath);
String dir = filePath;
/**
* 谁让java就是nb,只能监听目录
*/
if(!file.isDirectory()) {
dir = file.getParent();
}
/**
* 开启监听
*/
Observers.startWatch(ObserverType.LOCAL_FILE, this, dir);
}
} catch (Exception e) {
logger.error("init refresh bean error",e);
}
}
/**
* 刷新spring中被@RefreshScope修饰的类或者方法中涉及到配置的改变,注意该类可能被@Component修饰,也有可能被@Configuration修饰
* 1.类中被@Value修饰的成员变量需要重新修改更新后的值(
* 2.类中使用@Bean修饰的方法,如果该方法需要的参数中有其他被@RefreshScope修饰的类的对象,这个方法生成的类也会一同改变
* 3.类中使用@Bean修饰的方法循环依赖相互对象会报错,因为这种情况是属于构造方法层面的循环依赖,spring里也会报错,
* 所以我们也不需要考虑循环依赖
*/
private void init() throws ClassNotFoundException {
/**
* 将applicationContext转换为ConfigurableApplicationContext
*/
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
/**
* 获取bean工厂并转换为DefaultListableBeanFactory
*/
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
/**
* 获取工厂里的所有beanDefinition,BeanDefinition作为spring管理的对象的创建模板,可以类比java中的Class对象,
*/
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for(String beanName : beanDefinitionNames) {
BeanDefinition bd = defaultListableBeanFactory.getBeanDefinition(beanName);
/**
* 使用注解加载到spring中的对象都属于AnnotatedBeanDefinition,毕竟要实现刷新配置也要使用@RefreshScope
* 没有人丧心病狂的使用xml申明一个bean并且在类中加一个@RefreshScope吧,这里就不考虑非注解方式加载的情况了
*/
if(bd instanceof AnnotatedBeanDefinition) {
/**
* 得到工厂方法的元信息,使用@Bean修饰的方法放入beanDefinitionMap的beanDefinition对象这个值都不会为空
*/
MethodMetadata factoryMethodMeta = ((AnnotatedBeanDefinition) bd).getFactoryMethodMetadata();
/**
* 如果不为空,则该对象是使用@Bean在方法上修饰产生的
*/
if(factoryMethodMeta != null) {
/**
* 如果该方法没有被@RefreshScope注解修饰,则跳过
*/
if(!factoryMethodMeta.isAnnotated(REFRESH_SCOPE_ANNOTATION_NAME)) {
continue;
}
/**
* 拿到未被代理的Class对象,如果@Bean修饰的方法在@Configuration修饰的类中,会由于存在cglib代理的关系
* 拿不到原始的Method对象
*/
Class> clazz = Class.forName(factoryMethodMeta.getDeclaringClassName());
Method[] methods = clazz.getDeclaredMethods();
/**
* 循环从class对象中拿到的所有方法对象,找到当前方法并且被@RefreshScope修饰的方法构造invoker对象
* 放入执行器map中,为后续处理@ConfigurationProperties做准备
*/
for(Method m : methods) {
if(factoryMethodMeta.getMethodName().equals(m.getName()) && m.isAnnotationPresent(RefreshScope.class)) {
refreshScopeBeanInvokorMap.put(Class.forName(factoryMethodMeta.getReturnTypeName()),
new SpringAnnotatedRefreshScopeBeanInvoker(true, defaultListableBeanFactory, beanName, (AnnotatedBeanDefinition)bd, clazz,m));
}
}
} else {
/**
* 这里显然是正常的非@Bean注解产生的bd对象了,拿到元信息判断是否被@RefreshScope修饰,这里可不能用
* bd.getClassName这个拿到的是代理对象,里面自己定义的属性已经被去掉了,更加不可能拿到被@Value修饰
* 的属性了
*/
AnnotationMetadata at = ((AnnotatedBeanDefinition) bd).getMetadata();
if(at.isAnnotated(REFRESH_SCOPE_ANNOTATION_NAME)) {
Class> clazz = Class.forName(at.getClassName());
/**
* 先放入执行器map,后续循环处理,其实为啥要做
*/
refreshScopeBeanInvokorMap.put(clazz,
new SpringAnnotatedRefreshScopeBeanInvoker(false, defaultListableBeanFactory, beanName, (AnnotatedBeanDefinition)bd, clazz,null));
}
}
}
}
}
/**
* 根据传入属性刷新spring容器中的配置
* @param props
*/
public void refreshConfig(Map props) throws InvocationTargetException, IllegalAccessException {
if(props.isEmpty() || refreshScopeBeanInvokorMap.isEmpty()) {
return;
}
/**
* 循环遍历要刷新的执行器map,这里为啥没用foreach就是因为没法向外抛异常,很让人烦躁
*/
for(Iterator, SpringAnnotatedRefreshScopeBeanInvoker>> iter = refreshScopeBeanInvokorMap.entrySet().iterator(); iter.hasNext();) {
Map.Entry, SpringAnnotatedRefreshScopeBeanInvoker> entry = iter.next();
SpringAnnotatedRefreshScopeBeanInvoker invoker = entry.getValue();
boolean isMethod = invoker.isMethod();
/**
* 判断执行器是不是代表的一个@Bean修饰的方法
*/
if(isMethod) {
/**
* 使用执行器将属性刷新到@Bean修饰的方法产生的对象中,这里暂时不需要处理,仅仅@Value注解不需要处理@Bean
* 修饰的方法
* TODO
*/
} else {
/**
* 使用执行器将属性刷新到对象中
*/
invoker.refreshPropsIntoField(props);
}
}
}
public static void setFilePath(String filePath) {
SpringBootContext.filePath = filePath;
}
}
复制代码
如上代码中,写了很详细的注释,主要思路就是实现ApplicationContextAware接口让springboot初始化的时候给我注入一个applicationContext,进而可以遍历所有的BeanDefinition。先在获得了applicationContext的时候找到被@RefreshScope修饰的类或者方法块放入全局的map中。然后在配置修改的监听收到事件后触发刷新配置,刷新配置的过程就是使用反射强行修改实例的值,由于spring管理的对象基本都是单例的,假设spring容器中有两个对象A和B,其中B引用了A,那么修改A的属性,那么引用A的B对象同时也会跟着修改,因为B里引用的A已经变了,但是引用地址没变,再次调用A的方法实际上是调用了改变后的A的方法。写程序的过程实际上是运用分治法将一个大任务拆成多个小任务分别委派给多个类处理,最后汇总返回。每个类都是对调用方透明的封装体,各自的修改后的效果也最终会反应到调用方上来。回到正题,核心类中用到的封装好的执行器类如下
复制代码
package com.rdpaas.easyconfig.context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
/**
* 封装的执行器,主要负责真正修改属性值
* @author rongdi
* @date 2019-09-21 10:10:01
*/
public class SpringAnnotatedRefreshScopeBeanInvoker {
private Logger logger = LoggerFactory.getLogger(SpringAnnotatedRefreshScopeBeanInvoker.class);
private final static String VALUE_REGEX = "\\$\\{(.*)}";
private final static String COLON = ":";
private DefaultListableBeanFactory defaultListableBeanFactory;
private boolean isMethod = false;
private String beanName;
private AnnotatedBeanDefinition abd;
private Class> clazz;
private Method method;
public SpringAnnotatedRefreshScopeBeanInvoker(boolean isMethod, DefaultListableBeanFactory defaultListableBeanFactory, String beanName, A