https://gitee.com/sleepywang1024/easyconfig/blob/master/developer-guide.md
easyconfig开发者指南
面向需要深入了解easyconfig使用的研发人员,请关注本文内容。普通研发用户请你移驾quickstart。
quickstart 提到的接入方法通常已经满足需要,您也可以自定义、更灵活地进行此项工作。根本上讲,easyconfig提供两个类供业务应用系统接入和集成。这两个类分别实现配参的刷新和配参的管理能力。
jm.easyconfig.CfgContextEnvironmentAware用于注册管理系统中需要配参的Spring管理的Bean,以及后续配参更新时的数据更新。CfgContextEnvironmentAware自身必须是一个Spring Bean,且只能有一个。
把CfgContextEnvironmentAware注册为Spring的一个Bean就完成了配参刷新能力的初始化。
jm.easyconfig.CfgAdminServlet是一个标准的Servlet,用于提供管理界面给用户访问。把它注册为一个WEB Servlet则完成了配参管理控制台的初始化。Spring MVC项目通常在web.xml文件中配置。
easyconfig源码中有一个类CfgAutoConfiguration,就是负责完成上面刷新Bean和管理Servlet的注册,如下示意:
@ConfigurationpublicclassCfgAutoConfiguration{@Bean@ConditionalOnMissingBean(CfgContextEnvironmentAware.class)CfgContextEnvironmentAwarecfgContextEnvironmentAware(){returnnewCfgContextEnvironmentAware();}@Bean@ConditionalOnMissingBean(CfgServletRegistrationBean.class)publicCfgServletRegistrationBeancfgAdminServlet(){returnnewCfgServletRegistrationBean("/easyconfig-admin/*");}}
对于SpringBoot项目,可以通过导入CfgAutoConfiguration更快捷地接入,如下所示:
@Import({jm.easyconfig.CfgAutoConfiguration.class})
这样一条指令就完成easyconfig系统集成和使用,Import指令通常放在主应用(@SpringBootApplication)的class标注处,或任意**@Configuration**类的class标注处导入。
注:出于spring版本兼容性考虑,CfgAutoConfiguration代码被注释掉了,需要的话你可以加回去。
请快速浏览quickstart 介绍。
值得注意的是:并不是所有的@Value标注都被动态刷新支持。@Value在Spring中应用更广泛,例如构造方法的参数,SpEL表达式等,但这些不是配参管理面临的问题,不被easyconfig支持。动态刷新限于属性占位符${...}。
出于兼容性、习惯性考虑,easyconfig允许关闭@Value的动态刷新能力,属性easyconfig.value.enabled设置为false即可。
easyconfig.value.enabled=false
注:@Value动态刷新默认是开启的,明确配置为false才可以禁用。
@EasyValue标注提供@Value标注的value()属性外,提供了name()属性,用于为配参指定供人阅读的名字,这为后续配参管理提供很大便利。示例:
@EasyValue(value="${test.port:8080}",name="XXX服务端口")publicvoidsetPort(intport){this.port=port;}
注:在加入到动态配参管理时,name属性会随属性key和value一起自动导入,简化管理和文档工作。
除此name属性之外,@EasyValue在以下方面与@Value有所不同。
@Value引用的属性,如果Spring传统方式(如默认值、配参文件等)没有提供,则Spring会启动失败,@EasyValue则没有这个限制。
@Value支持的数据类型更少,主要是简单数据类型,以及简单数据类型的数组或列表。@EasyValue则不受这个限制,详情请参考支持的数据类型
@EasyValue标注的属性在Spring容器生命周期的postProcessBeforeInitialization阶段赋值,@Value在Spring更早一点就被初始化。
@ValueExt标注为方便配参管理增加了name()属性,其它行为与@Value完全形同,包括@Value若干限制。
简单地讲,就是可以标注在类的field变量,setter方法上面,可以是父类和子类,可以是任意的可见性,从public公开到private私有。
注意:
在setter方法上添加标注,并不需要提供一个getter方法,但建议你同时提供getter方法,便于easyconfig检测数值是否发生变化,避免不必要的更新。
不支持在接口方法上添加动态刷新的标注,认为不应该对配参这么做。
推荐使用**@EasyValue**标注,可以继续使用@Value。
在field上面使用标注更简单,在property的getter方法上使用标注更灵活、强大、健壮。
伴随配参变化需要进行一些处理的情景,如各类数据检查及转换、后台服务等重新初始化等,建议标注在setter上。
而对于更复杂的情景,如果依赖于多个配参的初始化工作,建议使用@PostRefresh标注初始化方法,更多信息参见Bean属性赋值及刷新次序。这种情景可能意味着配参设计不合理,如果不是遗留系统,标注setter方法结合复杂数据类型能更方便地解决这个问题。
通过前面提到的标注获得配参的数值,必须依附于一个Spring管理的Bean对象(@Bean、@Component、@Service、 @Controller @RestController、 @SpringApplicationBoot等)。但并不是所有的应用都希望这样,下面讨论通过API的形式更灵活的访问动态配参。
通过Spring内置的Environment可以访问easyconfig管理的配参,返回联机更新的最新内容。
受限于Environment,这种方式不支持复杂数据类型。需要得到复杂数据类型,可以先获取String类型配参,自行转换为复杂类型。下面示例中就是使用工具类CfgHelper做的Json的文本到Bean的转换。
@AutowiredEnvironmentenvironment;voidaccessByEnvironment(){StringbeanListText=environment.getProperty("complex.beanlist");DataBeanSample[]beanList=CfgHelper.jsonRead(beanListText,DataBeanSample[].class);}
对象如果不被Spring管理,但还希望其被@EasyValue等标注的属性联机更新,可以借助CfgContextEnvironmentAware的register方法,示例如下:
@AutowiredCfgContextEnvironmentAwareaware;publicstaticclassPropSample{@EasyValue("${complex.beanlist}")DataBeanSample[]beanList;}voidaccessByBean(){PropSampleprop=newPropSample();aware.register(prop);System.out.println("accessByBean:"+Arrays.asList(prop.beanList));}
如果不是@Autowired注入CfgContextEnvironmentAware,编程可以通过**CfgContextEnvironmentAware.getSharedInstance()**的方式获得。
easyconfig支持比Spring更为丰富的数据类型,保持兼容和一致,会优先使用Spring内置的类型转换服务,而复杂类型使用JSON解析器进行转换。
原子数据类型及其包装类,如String、boolean(Boolean)、int(Integer)等
常用简单类型,如File、URI、URL、BigDecimal等
原子类型、简单类型的数组,以及字符串的列表,如int[], String[]等,ArrayList (JAVA及Spring限制导致,其它元素类型的集合请参考下面的复杂数据类型)。文本的书写形式是元素之间以","号分割,例如a,b,c表示三个字符串(a/b/c)的数组或List。
复杂数据类型(推荐),如任意类型的数组、任意Bean类型、集合、Map(Key-Value)等。文本的书写格式要符合JSON,数组以[...]包裹,对象以{...}包裹。
注:
前三种类型是Spring配参自身就支持的,easyconfig保持继承和兼容。
第四种类型--任意复杂数据类型,是easyconfig重点扩展支持的内容,简单、强大、易用。
理论上,第四种类型覆盖前三种类型,easyconfig对它们是使用不同的是不同类型转换器。
复杂数据类型使用起来一点也不复杂,应该说和其它数据类型的使用方式一样,并不需研发人员为此额外做工作。但有一些技巧和注意事项,能帮助研发童鞋更好地使用:
底层转换使用JSON机制,JSON转换器保持与Spring使同一个(Spring可以使用多个Json转换器类库,并且可以定制)。复杂类型具体表现可能会受到JSON转换器的约束(不同担心这个,目前JSON转换很成熟)。
复杂类型是一个集合时,建议使用数组类型替代。这是因为JAVA语言对泛型支持的限制,导致JSON转换器对集合支持不是很好。下面示例中,由于runtime知道listLimited变量是List.class但不知道它的元素是DataBean.class类型,导致赋值给listLimited的数值是一个ArrayList>类型,但arrayStyle变量就没有这个问题,可以正确地赋值。
...publicstaticclassDataBean{publicStringname;publicintage;}@EasyValue("${test.datalist}")publicDataBean[]arrayStyle; @EasyValue("${test.datalist}")publicList<DataBean>listLimited;...
改用List类型为array类型实际上并不会给研发带来什么不便,但却很容易解决JSON转换的困惑。
复杂类型是一个Bean(class)时,最为灵活且强大。Bean拥有的属性也可以使用List类型,不受前一条建议的限制。例如下例中List list作为复杂配参对象的一个属性,这是没有问题的。
...publicstaticclassComplexBean{publicList<DataBean>list;publicintanyvalue=9;Stringanything="hello easyconfig";} @EasyValue("${test.complex}")publicComplexBeanbeanStyle;...
复杂类型的配参设置必须是JSON格式。这点很容易理解,easyconfig的管理控制台,也能很好地支持JSON类型。但如果你也希望在属性文件(properties)文件中配置,请注意不要换行 或者 最后一行外每行以反斜杠 \ 结尾,如下所示。
...test.datalist=[\{"name":"zs","age":21},\{"name":"lis","age":32}]...
注:easyconfig管理控制台提供工具支持把多行数据平铺到一行。
复杂类型的JSON转换技巧。JSON转换过程中,有些数据类型容易出现兼容性问题,如datetime。解决此类问题的方法通常是对JSON转换器进行各类配置,添加定置的对象串行化/反串行化组件,您可以同解决任意WEB项目遇到的问题解决此处复杂类型转换。easyconfig使用spring内置的转换器,你对此转换器的配置会被easyconfig继承。
通常情况下,easyconfig不要你对动态配参的数据源做任何配置,easyconfig会在启动时自动检测并做好相关初始化动作。
通常情况下,easyconfig会自动检测系统中已经存在的DataSource,把这个数据源当作储存配参的数据源,并在数据源自动检测,需要时自动创建动态配参需要的两张数据库表。检测数据源的方式是从Spring的ApplicationContext中检查是否存在DataSource.class创建的Bean。
easyconfig允许研发同学通过property指定配参使用的数据源,指定数据源优先级高于自动检测。这是考虑到有些多数据源的系统、用户动态配置数据源的系统、以及非SQL数据源的系统等,无论是多DataSource或者没有DataSource,通过配参指定配置数据源准确地解决配参的存储问题。
easyconfig指定了四个property用于配置数据源,含义同spring.datasource下的对应的属性,示例如下:
easyconfig.datasource.url=jdbc:mysql://localhost:3306/testeasyconfig.datasource.username=rooteasyconfig.datasource.password=easyconfig.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
注: 显然这几个参数只能通过Spring工程的传统技术配置:properties或者yml文件进行,虽然easyconfig的许多配参支持动态修改,但数据源连接参数不支持动态配参。
动态配参存储在数据源数据库的一张表中,另一张表存储配参的变更历史。系统启动时,easyconfig会检测相关数据表是否创建,没有创建时会自动创建,完成数据源的初始化。
如果你不希望每次启动做这种检测,可以通过一个配参关闭此检测,如下所示:
easyconfig.auto-init-schema=false
easyconfig创建的两张数据表的结构都很简单,如下所示。config_item_main存储当前的动态配参,可以手工或任意方式维护,但不建议这样做,直接修改数据库可能需要若干分钟才能在系统中生效。
字段代码字段名称说明
item_code配参的唯一代码@Value或@EasyValue引用使用,非空唯一、表的主键
item_name配参的名字(简称)
item_value参数的数值@Value或@EasyValue标注的属性或setter方法,得到的数值
item_schema配参的格式说明扩展json schema方式的配参说明,复杂类型的格式规范
item_status配参的状态说明二进制第一位为1时表示删除,0表示正常,第二位为1表示密文,0表示明文
updated_time配参最后更新时间戳
updated_user配参最后更新用户
updated_history更新历史
config_item_main_history( 配参变更历史记录表)
字段代码字段名称说明
changed_id唯一IDID为自增数字,主键,满足关系数据库要求所加
changed_summary变更汇总信息
changed_content变更的详细内容
changed_time变更时间
changed_user变更用户
changed_status变更状态
注:
easyconfig支持单数据源多套配参,会在数据源中会创建多套数据表,表名字格式为 config_item_main中main被替换为相应名称。
配置的方式是由easyconfig.client属性指定,默认为main。
easyconfig.client=main
当配参被修改后,如何快速及时地更新到所有使用这些配参的系统中,常用机制无外乎主动轮询 或 被动通知,easyconfig支持这两种方式,用户基于需要选择其中之一。
优点是简单、不需要额外中间件、jar包支持。缺点是时效性差、对数据源造成一些负载。
优点是时效性、扩展性比较好。缺点是需要rabbitmq中间件和访问jar包。
系统配参easyconfig.updater用于指定使用那个配参更新器,默认值为auto,自动检测系统中是否存在rabbitmq,存在则使用,不存在则使用轮询方式。
可以明确指定更新器,polling为轮询方式、rabbitmq为采用rabbitmq的消息机制。
easyconfig.updater=auto
详细介绍请参考更新器详情。
搞明白此小结需要清楚了解Spring对Bean生命周期管理,供资深用户参考。
在setEnvironment(Environment)期间,获取系统的本地缓存的动态配参,保存到environment对象中。
在setApplicationContext(ApplicationContext)期间,从数据源获取动态配参,覆盖environment对象的动态配参。
在postProcessBeforeInitialization期间,检测Spring管理的每一个bean对象内的标注,如@EasyValue、@ValueExt、@Value等,注册相关信息到内存中,并解析参数数值、做相应的类型转换,对相应属性赋值。
当一个bean的所有属性赋值完成后,对bean进行整体初始化。
针对每一个配参,更新所有引用此配参的bean对象的相应属性。
当所有被更新配参的所有bean对象都被更新完毕后,针对实际做出修改的bean对象,依次对bean进行整体初始化。
Bean的整体初始化指Bean的各个属性被赋值完成后,被触发的初始化操作。包括
InitializingBean接口的afterPropertiesSet方法
@PostConstruct标注的方法
@PostRefresh标注的方法,这个标注是easyconfig引入并触发的。
基本的规则是:
在系统加载的初始化阶段,afterPropertiesSet 和 @PostConstruct会优先于@PostRefresh。也就是这个阶段只有afterPropertiesSet 和 @PostConstruct都没定义时,才会调用@PostRefresh标注的方法。而没有afterPropertiesSet 和 @PostConstruct时,才会去调用标注@PostRefresh。并且这个阶段对afterPropertiesSet 和 @PostConstruct的调用,不是easyconfig而是由spring完成。
在配参的动态刷新阶段,@PostRefresh会优先于afterPropertiesSet 和 @PostConstruct。也就是这个阶段只要标注了@PostRefresh方法,就只会调用@PostRefresh,而不再会调用afterPropertiesSet 和 @PostConstruct。而没有标注@PostRefresh时,才会去调用afterPropertiesSet 和 @PostConstruct。
easyconfig内置管理控制台用于配参的查看、修改、恢复等操作,出于安全和审计的需要,对配参的修改与恢复需要得到严格的管理:谁有权限进行什么操作,easyconfig会自动记录谁进行相关操作。
easyconfig内置用户权限控制,也可以集成宿主应用用户权限,此时需要系统告知easyconfig当前操作的用户是谁。
easyconfig提供四种安全控制方式:none、self、host、mixture,通过配参easyconfig.security.mode控制,默认为self,即:
easyconfig.security.mode=self
none:没有安全控制,easyconfig没有为此做工作;
self: easyconfig控制安全,创建用户、授权、登录登出等;
host: 宿主应用控制安全,提供访问easyconfig的用户及安全信息;
mixture:easyconfig和宿主应用共同控制安全,宿主应用提供系统当前登录用户,由easyconfig控制能对哪些配参做哪些访问。
none是没有认证和授权,self是easyconfig控制认证授权、host是宿主应用控制认证授权,mixture是宿主提供认证,easyconfig提供授权。
另:easyconfig管理控制台是前后端分离应用,宿主应用可以通过应用服务器的filter等机制,自行控制对eayconfig的请求。此种情境下,任何控制自然不受easyconfig限制,或easyconfig.security.mode选项的限制。
easyconfig.security.mode为host 或 mixture时,需要宿主应用定制安全能力。具体方式为:
注册一个bean对象,实现接口CfgContextUser, 示例如下:
@AutowiredprivateHttpSessionsession; @BeanpublicCfgContextUsercontextUser(){returnnewCfgContextUser(){@OverridepublicStringusername(){HostUseruser=session.getAttribute("user");returnuser==null?"":user.getEmail();}@OverridepublicAdminUseruser(){AdminUseruser=newAdminUser();user.username="hello";user.func=AdminUser.AUTH_ALL;returnuser;}};}
当host模式时,需要实现AdminUser user()方法,返回访问easyconfig的当前用户信息,关键信息是username、func、acl。
username:用户名称,唯一,登记在配参修改日志。
func: 功能权限,以及是否启动数据权限。
acl: 数据权限的访问控制表,形式同easyconfig的数据权限配置内容。
当mixture模式时,需要实现String username()方法,返回访问easyconfig的当前username,权限信息取自easyconfig配置的同名用户的权限。
出于安全考虑,easyconfig.security.mode只能通过属性文件等传统方式配置,不能通过easyconfig自身的动态配参配置。
配参环境管理是一项非常重要和有效的工具,支持应用系统同一时间不同实例使用不同配参运行。其设计目标如下:
空间维度的配参管理。相对于“时间维度”:不同时间内使用不同的参数运行系统,“空间维度”在同一时间让不同的应用实例节点采用不同的参数运行。
支持不同研发环境不同配参,如开发、测试、准生产。
简化不同实例节点共同参数、以及特定参数的配置(假设多数相同、少量不同)。
提供的主要能力如下:
为所有实例节点提供名字为default的默认profile,但优先级最低;
基于启动参数指定应用节点生效的profiles,可以多个;
同一个配参在多个profile中存在,后序定义覆盖前序定义;
内置特定名字$node的profile,代表为每个实例节点启动节点专用的profile;
支持自定义动态profile定义。
easyconfig.profiles.active用于指定easyconfig为应用节点启动哪些配置环境。可指定多个,多个profile中有相同配参时,后序profile定义优先于前序profile中定义。
easyconfig.profiles.active=prod,$node
profile指一组配参代表的环境,环境与配参可以任意组合;
default为内置的默认profile,无需指定自动存在且自动生效,优先级最低;也就是说default是默认的第一个active的profile。
$node是另一个特殊的profile,指应用实例节点专属profile,每启动一个应用实例,就会基于此节点的ip/port构建一个专属profile。
有了$node,就可以方便地为每一个节点配置不同的参数。
$node并不会自动启用,需要启动应用时,通过easyconfig.profiles.active明确指定才能激活。
easyconfig还提供了类似于 $node环境动态扩展机制。
前后台分离架构,前端也有配参支持的的需求,easyconfig对前端配参提供简单支持。
通过以下属性开启或关闭前端访问,默认为开启。
easyconfig.frontend.enabled=false
访问路径方式:
方法:GET
路径:/${easyconfig-admin}/get-prop
参数1:code为配参属性代码
返回值:JSON结构 {"error":0,"data":"..."}。error不为0则表示错误,为0正常返回,此时data是对应配参的文本数值。
前端访问配参支持相对简单许多,这是考虑到:
使用场景较少,历史负担小;
保障数据安全,不易提供批量访问接口;
复杂配参选用JSON格式支持,无需要访问多次多个配参;
网络访问的延迟、性能问题,不易大规模使用;
配参经常涉及一些敏感信息,如网络地址、用户名、密码等,提高它们的安全必要的,避免不恰当管理造成泄露。easyconfig对此提供安全增强,基于实际情况,选用下面任意三种方式。
在管理控制台设置配参加密保存,系统自动得到解密后的明文信息。详情请参考用户手册。
对于一些基础的,在easyconfig启动之前就需要的配参,例如数据库连接,提供另一种加密设置方式。如下示例:
spring.datasource.url=qTe2SoazP1vp918zK6U7xweHgfJR+OzZhewQfC32LGUST1rNIoCI4pmbeJgW03azWIkTudMU8yS53a03daznZFw++Sc1jcN9nXYWWgQGh4tHhJ47HAlsvz7wXZJ51I4588j/r7x6phnI10hERXgqP9MjU473MeoiIKKny3W28lo1aPCSU4RlW3yNwGGgbludspring.datasource.password=9SZS6s/utwXZtFhOAe3ASg==easyconfig.cipher-keys=spring.datasource.url,spring.datasource.password
说明:
把加密的属性key以逗号分隔的形式设置到easyconfig.cipher-keys。
用密文设置相应属性值,系统得到明文数值,即使Spring使用的参数也是如此。
密文通过管理控制台 或 jar命令行 中的加解密工具生成,请参考用户手册。
如果是数字等类型的属性配参,加密之后就变成文本了,编辑器会提示错误,但不影响使用。
easyconfig.CfgHelper类中提供了两个加解密的辅助方法,研发同学可以编码的方式灵活处理加解密。
publicstaticStringencrypt(Stringcontent)...publicstaticStringdecrypt(Stringcontent)...
easyconfig目的是对配参安全进行增强,达到一种合理水平,而不追求更高安全。实际上由于easyconfig被设计成组件,对于对源码进行反编译的专业破解者来讲,安全性是不够的。
easyconfig采用AES加密算法进行,系统可以指定一个salt来提升及定置化加密,如下:
easyconfig.cipher-salt=myPrivateToken
注意:easyconfig.cipher-salt设置后不能修改,否则加密的配参就不能解密。更换salt需要手工把所有密文改为明后再进行。
@Value标注可以指定默认值,例如${server.port:8080}表示属性server.port没有配置时,默认值为8080。本地的properties文件或yml文件对相关属性进行配置,就会覆盖@Value的默认值。
easyconfig追加了另外两套机制:本地缓存和配参数据源。本地缓存是系统每隔一定时间缓存下来的配参数据源数据,当系统启动时配参数据源不可用时,本地缓存会的优先级会高于本地属性文件。本地缓存默认开启,但可以关闭,如下:
easyconfig.localcache.enabled=false
配参数据源是动态配参管理的数据源,通过它集中管理系统的各个运行实例的配参,动态修改和更新,而不用重新启动服务。
这几套机制结合起来使用,避免环境短期故障导致对用户的服务中断,或者导致研发工作的中断,毕竟配参是系统运行的基础和前提,任何中断都是麻烦。
系统启动时,easyconfig会检查配参数据源,从数据源获取最新的参数。如果失败的话,可以启动成功,在配参会保留到本地缓存状态。
系统运行过程中,数据源或者redis服务中断,easyconfig会继续按照约定周期检查更新,服务一旦恢复,easyconfig会随即恢复。
easyconfig 努力避免单记录错误对整体服务造成影响,让单条配参记录错误影响限制在单条范围内。
通过配参的格式推导、数据校验、配参探查能减少出错机会。
### 通过变更日志及恢复减小错误后果
多种变更恢复机制,保障配参维护错误能快速恢复到正确状态上。