依赖配置中心实现注有@ConfigurationProperties的bean相关属性刷新
配置中心是什么
配置中心,通过key=value的形式存储环境变量。配置中心的属性做了修改,项目中可以通过配置中心的依赖(sdk)立即感知到。需要做的就是如何在属性发生变化时,改变带有@ConfigurationProperties的bean的相关属性。
配置中心原理
在读配置中心源码的时候发现,里面维护了一个Environment,以及ZookeeperPropertySource。当配置中心属性发生变化的时候,清空ZookeeperPropertySource,并放入最新的属性值。
public class ZookeeperPropertySource extends EnumerablePropertySource<Properties>
ZookeeperPropertySource重写了equals和hahscode方法,根据这两个方法可以判定配置中心是否修改了属性。
动态刷新bean属性原理
实现原理图
动态刷新bean父类
public abstract class BaseConfigCenterBean implements InitializingBean { private static Logger LOGGER = LoggerFactory.getLogger(BaseConfigCenterBean.class); //配置中心是否生效
protected boolean cfgCenterEffect = false; public boolean isCfgCenterEffect() {
this.checkCfgCenterEffect();
return cfgCenterEffect;
} private void checkCfgCenterEffect() {
boolean tmpCfgCenterEffect = !Objects.isNull(ConfigHelper.getEnvironment());
if (tmpCfgCenterEffect) {// NOSONAR
String value = (String) ConfigHelper.getZookeeperPropertySource().getProperty("cfg.center.effect");
if (StringUtils.isBlank(value)) {
tmpCfgCenterEffect = false;
} else {
tmpCfgCenterEffect = Boolean.valueOf(value);
}
} cfgCenterEffect = tmpCfgCenterEffect; if (cfgCenterEffect) {
String prefix = this.getConfigPrefix();
cfgCenterEffect = Arrays.stream(ConfigHelper.getZookeeperPropertySource().getPropertyNames())
.filter(keyName -> keyName.indexOf(prefix) == 0)
.count() > 0;
if (!cfgCenterEffect) {
LOGGER.info(String.format("配置中心没有发现模块=%s, prefix=%s的配置,将使用本地配置...", this.getModuleName(), prefix));
}
}
} /**
* 绑定自身目标
**/
protected void doBind() {
Class<? extends BaseConfigCenterBean> clazz = this.getClass();
if (AopUtils.isCglibProxy(this)) {
clazz = (Class<? extends BaseConfigCenterBean>) AopUtils.getTargetClass(this);
}
BaseConfigCenterBean target = binding(isCfgCenterEffect(), clazz, this.getDefaultResourcePath());
this.copyProperties(target);
} private void copyProperties(BaseConfigCenterBean target) {
ReflectionUtils.doWithFields(this.getClass(), field -> {
field.setAccessible(true);
field.set(this, field.get(target));
}, field -> AnnotatedElementUtils.isAnnotated(field, ConfigField.class));
} /**
* 绑定其他目标
*
* @param clazz 目标类
**/
protected <T> T doBind(Class<T> clazz) {
T target = binding(isCfgCenterEffect(), clazz, this.getDefaultResourcePath());
if (target instanceof InitializingBean) {
try {
((InitializingBean) target).afterPropertiesSet();
} catch (Exception e) {
LOGGER.error(String.format("属性初始化失败[afterPropertiesSet], class=%s", ClassUtils.getSimpleName(clazz), e));
}
}
return target;
} private <T> T binding(boolean cfgCenterEffect, Class<T> clazz, String defaultResourcePath) {
Optional<PropertySource> propertySource = Optional.empty(); if (cfgCenterEffect) {
propertySource = Optional.ofNullable(ConfigHelper.getZookeeperPropertySource());
} else {
Optional<ResourcePropertySource> resourcePropertySource = ResourceUtils.getResourcePropertySource(defaultResourcePath);
if (resourcePropertySource.isPresent()) {
propertySource = Optional.ofNullable(resourcePropertySource.get());
}
} if (propertySource.isPresent()) {
T target;
try {
target = RelaxedConfigurationBinder
.with(clazz)
.setPropertySources(propertySource.get())
.doBind();
} catch (GeneralException e) {
LOGGER.error(String.format("属性绑定失败, class=%s", ClassUtils.getSimpleName(clazz)), e);
return null;
}
return target;
}
return null;
} @Override
public void afterPropertiesSet() {
Class<?> target = this.getClass();
if (AopUtils.isAopProxy(this)) {
target = AopUtils.getTargetClass(this);
}
LOGGER.info(String.format("%s->%s模块引入配置中心%s...", this.getModuleName(), ClassUtils.getSimpleName(target), (isCfgCenterEffect() ? "生效" : "无效")));
} public String getModuleName() {
return StringUtils.EMPTY;
} @Subscribe
public void listenRefreshEvent(ConfigCenterUtils.ConfigRefreshEvent refreshEvent) {
if (!refreshEvent.getModuleName().equals(this.getModuleName())) {
this.refreshForEvent();
}
} //通过事件进行刷新
public abstract void refreshForEvent(); //获取本地配置默认路径
public abstract String getDefaultResourcePath(); //获取配置属性的公共前缀
public abstract String getConfigPrefix();
}
1、isCfgCenterEffect方法主要判断项目是否接入了配置中心并且配置中心配有bean中相关的属性。
2、binding方法主要根据isCfgCenterEffect方法的返回值去加载配置中心的properties还是本地的properties。
3、getDefaultResourcePath是主要是获取本地资源的默认路径(在没有接入配置中心的情况下)。
4、getConfigPrefix方法返回bean中配置属性的公共前缀(等同于@ConfigurationProperties中的prefix属性)。
5、refreshForEvent方法主要是在某个bean感知到配置中心更新属性时异步通知其他bean进行属性的更新。
bean属性绑定工具类
动态将propertysource绑定到带有@ConfigurationProperties注解的bean中。
参考 org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
public class RelaxedConfigurationBinder<T> {
private final PropertiesConfigurationFactory<T> factory; public RelaxedConfigurationBinder(T object) {
this(new PropertiesConfigurationFactory<>(object));
} public RelaxedConfigurationBinder(Class<?> type) {
this(new PropertiesConfigurationFactory<>(type));
} public static <T> RelaxedConfigurationBinder<T> with(T object) {
return new RelaxedConfigurationBinder<>(object);
} public static <T> RelaxedConfigurationBinder<T> with(Class<T> type) {
return new RelaxedConfigurationBinder<>(type);
} public RelaxedConfigurationBinder(PropertiesConfigurationFactory<T> factory) {
this.factory = factory;
ConfigurationProperties properties = getMergedAnnotation(factory.getObjectType(), ConfigurationProperties.class);
javax.validation.Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
factory.setValidator(new SpringValidatorAdapter(validator));
factory.setConversionService(new DefaultConversionService());
if (!Objects.isNull(properties)) {//NOSONAR
factory.setIgnoreNestedProperties(properties.ignoreNestedProperties());
factory.setIgnoreInvalidFields(properties.ignoreInvalidFields());
factory.setIgnoreUnknownFields(properties.ignoreUnknownFields());
factory.setTargetName(properties.prefix());
factory.setExceptionIfInvalid(properties.exceptionIfInvalid());
}
} public RelaxedConfigurationBinder<T> setTargetName(String targetName) {
factory.setTargetName(targetName);
return this;
} public RelaxedConfigurationBinder<T> setPropertySources(PropertySource<?>... propertySources) {
MutablePropertySources sources = new MutablePropertySources();
for (PropertySource<?> propertySource : propertySources) {
sources.addLast(propertySource);
}
factory.setPropertySources(sources);
return this;
} public RelaxedConfigurationBinder<T> setPropertySources(Environment environment) {
factory.setPropertySources(((ConfigurableEnvironment) environment).getPropertySources());
return this;
} public RelaxedConfigurationBinder<T> setPropertySources(PropertySources propertySources) {
factory.setPropertySources(propertySources);
return this;
} public RelaxedConfigurationBinder<T> setConversionService(ConversionService conversionService) {
factory.setConversionService(conversionService);
return this;
} public RelaxedConfigurationBinder<T> setValidator(Validator validator) {
factory.setValidator(validator);
return this;
} public RelaxedConfigurationBinder<T> setResolvePlaceholders(boolean resolvePlaceholders) {
factory.setResolvePlaceholders(resolvePlaceholders);
return this;
} public T doBind() throws GeneralException {
try {
return factory.getObject();
} catch (Exception ex) {
throw new GeneralException("配置绑定失败!", ex);
}
}
}
配置中心工具类
public class ConfigCenterUtils {
private static Logger LOGGER = LoggerFactory.getLogger(ConfigCenterUtils.class); private static AsyncEventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8));//NOSONAR private static Properties cfgProperties; private static Environment environment; static {
cfgProperties = new Properties();
cfgProperties.putAll(ConfigHelper.getZookeeperPropertySource().getProperties());
} public static void setEnvironment(Environment environment) {
ConfigCenterUtils.environment = environment;
} public static String getValue(String name) {
try {
return PropertiesUtil.getValue(name);
} catch (Exception e) {
LOGGER.info("配置中心无效, property name=" + name, e);
}
if (Objects.isNull(environment)) {
LOGGER.info("environment无效,property name=" + name);
return StringUtils.EMPTY;
}
if (!environment.containsProperty(name)) {
LOGGER.info("environment无配置 property name=" + name);
return StringUtils.EMPTY;
}
return environment.getProperty(name);
} public synchronized static boolean propertySourceShouldRefresh(String moduleName, ZookeeperPropertySource newPropertySource) {
if (!cfgProperties.equals(newPropertySource.getProperties())) {
cfgProperties.clear();
cfgProperties.putAll(newPropertySource.getProperties());
eventBus.post(new ConfigRefreshEvent(moduleName));
return true;
}
return false;
} public static <T> T createToRefreshPropertiesBean(Class<T> clazz) {
Enhancer enhancer = new Enhancer();
// 设置代理对象父类
enhancer.setSuperclass(clazz);
// 标识Spring-generated proxies
enhancer.setInterfaces(new Class[]{SpringProxy.class});
// 设置增强
enhancer.setCallback((MethodInterceptor) (target, method, args, methodProxy) -> {
ToRefresh toRefresh = AnnotationUtils.findAnnotation(method, ToRefresh.class);
if (Objects.isNull(toRefresh) || StringUtils.isBlank(toRefresh.method())) {
return methodProxy.invokeSuper(target, args);
}
Method refreshMethod = ReflectionUtils.findMethod(target.getClass(), toRefresh.method());
if (Objects.isNull(refreshMethod)) {
return methodProxy.invokeSuper(target, args);
}
refreshMethod = BridgeMethodResolver.findBridgedMethod(refreshMethod);
refreshMethod.setAccessible(true);
refreshMethod.invoke(target, null);
return methodProxy.invokeSuper(target, args);
}); T target = (T) enhancer.create();// 创建代理对象 MethodIntrospector.selectMethods(clazz, (ReflectionUtils.MethodFilter) method -> AnnotatedElementUtils.isAnnotated(method, ToInitial.class))
.stream().findFirst().ifPresent(method -> {
method.setAccessible(true);
try {
method.invoke(target, null);
} catch (Exception e) {
LOGGER.error(String.format("初始化异常,class=%s ...", ClassUtils.getSimpleName(clazz)), e);
}
}); return target;
} public static void registerListener(BaseConfigCenterBean refreshableBean) {
eventBus.register(refreshableBean);
} public static class ConfigRefreshEvent {
private String moduleName; public ConfigRefreshEvent(String moduleName) {
this.moduleName = moduleName;
} public String getModuleName() {
return moduleName;
} public void setModuleName(String moduleName) {
this.moduleName = moduleName;
}
}
}
这个工具主要作用:
1、判断配置中心的属性是否发生了变化
2、为BaseConfigCenterBean子类创建代理类,使属性在getter方法时检测属性是否应该刷新。
3、提供将BaseConfigCenterBean类型的对象的注册为guava eventbus的监听对象,使之具有根据刷新事件自动刷新自身属性。
bean后置处理器
public class ConfigCenterBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (AnnotatedElementUtils.isAnnotated(bean.getClass(), ConfigCenterBean.class)) {
BaseConfigCenterBean refreshableBean = (BaseConfigCenterBean) ConfigCenterUtils.createToRefreshPropertiesBean(bean.getClass());
ConfigCenterUtils.registerListener(refreshableBean);
return refreshableBean;
}
return bean;
} @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
该后置处理器的作用是对所有BaseConfigCenterBean类型的bean进行处理,生成代理bean,并注册为guava eventbus相应的listener。
pojo属性绑定配置中心优雅方案1
@ConfigCenterBean
@ConfigurationProperties(prefix = "wx.temporary.qrcode")
@Component
public class QrcodeConstants extends BaseConfigCenterBean { private static Logger LOGGER = LoggerFactory.getLogger(QrcodeConstants.class); //渠道
@ConfigField //标识该属性来自配置中心
private List<Scene> channels; //业务
@ConfigField
private List<Scene> bizs; //业务和渠道映射关系
@ConfigField
private Map<String, String> biz2Channel; private Map<String, Scene> channelMap; private Map<String, Scene> bizMap; public List<Scene> getChannels() {
return channels;
} public void setChannels(List<Scene> channels) {
this.channels = channels;
} public List<Scene> getBizs() {
return bizs;
} public void setBizs(List<Scene> bizs) {
this.bizs = bizs;
} @ToRefresh(method = "toRefresh")
public Map<String, Scene> getChannelMap() {
return channelMap;
} @ToRefresh(method = "toRefresh")
public Map<String, Scene> getBizMap() {
return bizMap;
} @ToRefresh(method = "toRefresh")
public Map<String, String> getBiz2Channel() {
return biz2Channel;
} public void setBiz2Channel(Map<String, String> biz2Channel) {
this.biz2Channel = biz2Channel;
} @ToInitial
private void refreshQrcodeProperties() {
try {
super.doBind(); //属性处理
if (CollectionUtils.isEmpty(channels)) {
this.channelMap = Maps.newHashMap();
} else {
this.channelMap = channels.stream()
.collect(Collectors.toMap(channel -> channel.getType(), Function.identity()));
} if (CollectionUtils.isEmpty(bizs)) {
this.bizMap = Maps.newHashMap();
} else {
this.bizMap = bizs.stream()
.collect(Collectors.toMap(biz -> biz.getType(), Function.identity()));
} LOGGER.info(String.format("%s 刷新成功..., 当前配置=%s...", this.getModuleName(), this));
} catch (Exception e) {
LOGGER.error("QrcodeConstants 对象属性绑定失败...", e);
}
} private void toRefresh() {
try {
if (isCfgCenterEffect()) {
ZookeeperPropertySource propertySource = ConfigHelper.getZookeeperPropertySource();
if (ConfigCenterUtils.propertySourceShouldRefresh(this.getModuleName(), propertySource)) {
this.refreshQrcodeProperties();
}
}
} catch (Exception e) {
LOGGER.error("QrcodeConstants 对象属性刷新失败", e);
}
} //刷新事件调用
@Override
public void refreshForEvent() {
this.refreshQrcodeProperties();
} //本地资源文件
@Override
public String getDefaultResourcePath() {
return "config/qrcode.properties";
} //属性配置 公共前缀(和@ConfigurationProperties prefix 属性一致)
@Override
public String getConfigPrefix() {
return "wx.temporary.qrcode";
} //模块名称
@Override
public String getModuleName() {
return "微信临时二维码配置";
} @Override
public String toString() {
return ReflectionToStringBuilder.toString(this
, ToStringStyle.JSON_STYLE
, false
, false
, QrcodeConstants.class);
} public static class Scene {
private String type;
private String desc; public String getType() {
return type;
} public void setType(String type) {
this.type = type;
} public String getDesc() {
return desc;
} public void setDesc(String desc) {
this.desc = desc;
} @Override
public String toString() {
return ReflectionToStringBuilder.toString(this
, ToStringStyle.JSON_STYLE
, false
, false
, Scene.class);
}
}
}
pojo属性绑定配置中心优雅方案2
@ConfigCenterBean
@Component
public class QrcodeConstants extends BaseConfigCenterBean { private static Logger LOGGER = LoggerFactory.getLogger(QrcodeConstants.class); //业务和渠道映射关系
private Map<String, String> biz2Channel; //渠道
private Map<String, Scene> channelMap; //业务
private Map<String, Scene> bizMap; private QrcodeProperties qrcodeProperties; @ToRefresh(method = "toRefresh")
public Map<String, Scene> getChannelMap() {
return channelMap;
} @ToRefresh(method = "toRefresh")
public Map<String, Scene> getBizMap() {
return bizMap;
} @ToRefresh(method = "toRefresh")
public Map<String, String> getBiz2Channel() {
return biz2Channel;
} public void setBiz2Channel(Map<String, String> biz2Channel) {
this.biz2Channel = biz2Channel;
} public QrcodeProperties getRawQrcodeProperties() {
return qrcodeProperties;
} @ToInitial
private void refreshQrcodeProperties() {
try {
QrcodeProperties qrcodeProperties = super.doBind(QrcodeProperties.class);
if (Objects.isNull(qrcodeProperties)) {
LOGGER.error(String.format("没有加载到%s配置,请检查配置...", this.getModuleName()));
return;
} this.qrcodeProperties = qrcodeProperties; //属性处理
if (CollectionUtils.isEmpty(qrcodeProperties.channels)) {
this.channelMap = Maps.newHashMap();
} else {
this.channelMap = qrcodeProperties.channels.stream()
.collect(Collectors.toMap(channel -> channel.getType(), Function.identity()));
} if (CollectionUtils.isEmpty(qrcodeProperties.bizs)) {
this.bizMap = Maps.newHashMap();
} else {
this.bizMap = qrcodeProperties.bizs.stream()
.collect(Collectors.toMap(biz -> biz.getType(), Function.identity()));
} if (CollectionUtils.isEmpty(qrcodeProperties.getBiz2Channel())) {
this.biz2Channel = Maps.newHashMap();
} else {
this.biz2Channel = qrcodeProperties.getBiz2Channel();
} LOGGER.info(String.format("%s 刷新成功..., 当前配置=%s...", this.getModuleName(), this));
} catch (Exception e) {
LOGGER.error("QrcodeConstants 对象属性绑定失败...", e);
}
} private void toRefresh() {
try {
if (isCfgCenterEffect()) {
ZookeeperPropertySource propertySource = ConfigHelper.getZookeeperPropertySource();
if (ConfigCenterUtils.propertySourceShouldRefresh(this.getModuleName(), propertySource)) {
this.refreshQrcodeProperties();
}
}
} catch (Exception e) {
LOGGER.error("QrcodeConstants 对象属性刷新失败", e);
}
} @Override
public void refreshForEvent() {
this.refreshQrcodeProperties();
} @Override
public String getDefaultResourcePath() {
return "config/qrcode.properties";
} @Override
public String getConfigPrefix() {
return "wx.temporary.qrcode";
} @Override
public String getModuleName() {
return "微信临时二维码配置";
} @Override
public String toString() {
return new ToStringBuilder(this)
.append("biz2Channel", biz2Channel)
.append("channelMap", channelMap)
.append("bizMap", bizMap)
.toString();
} @ConfigurationProperties(prefix = "wx.temporary.qrcode")
public static class QrcodeProperties {
//渠道
private List<Scene> channels; //业务
private List<Scene> bizs; //业务和渠道映射关系
private Map<String, String> biz2Channel; public List<Scene> getChannels() {
return channels;
} public void setChannels(List<Scene> channels) {
this.channels = channels;
} public List<Scene> getBizs() {
return bizs;
} public void setBizs(List<Scene> bizs) {
this.bizs = bizs;
} public Map<String, String> getBiz2Channel() {
return biz2Channel;
} public void setBiz2Channel(Map<String, String> biz2Channel) {
this.biz2Channel = biz2Channel;
}
} public static class Scene {
private String type;
private String desc; public String getType() {
return type;
} public void setType(String type) {
this.type = type;
} public String getDesc() {
return desc;
} public void setDesc(String desc) {
this.desc = desc;
} @Override
public String toString() {
return ReflectionToStringBuilder.toString(this
, ToStringStyle.JSON_STYLE
, false
, false
, Scene.class);
}
}
}
方案1和方案2略有不同,针对一些属性,我们需要做一些逻辑处理。方案1中将源属性和逻辑之后的属性都放在了同一类中,方案二则是将源属性单独放到一个静态类中,最终处理过后的属性放在了目标类中。另外二者的doBind方法也是有区别的,仔细看一下BaseConfigCenterBean这个类就可以了。
就先分享这么多了,更多分享请关注我们的技术公众吧!!!
参考文章:算法和技术SHARING
依赖配置中心实现注有@ConfigurationProperties的bean相关属性刷新的更多相关文章
- Spring Boot 2.0 整合携程Apollo配置中心
原文:https://www.jianshu.com/p/23d695af7e80 Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境.不同集群的配置,配置修改后能够 ...
- SpringCloud学习系列之五-----配置中心(Config)和消息总线(Bus)完美使用版
前言 在上篇中介绍了SpringCloud Config的使用,本篇则介绍基于SpringCloud(基于SpringBoot2.x,.SpringCloud Finchley版)中的分布式配置中心( ...
- SpringCloud学习系列之四-----配置中心(Config)使用详解
前言 本篇主要介绍的是SpringCloud中的分布式配置中心(SpringCloud Config)的相关使用教程. SpringCloud Config Config 介绍 Spring Clou ...
- 基于zookeeper实现分布式配置中心(二)
上一篇(基于zookeeper实现分布式配置中心(一))讲述了zookeeper相关概念和工作原理.接下来根据zookeeper的特性,简单实现一个分布式配置中心. 配置中心的优势 1.各环境配置集中 ...
- Spring Cloud第十篇 | 分布式配置中心Config
本文是Spring Cloud专栏的第十篇文章,了解前九篇文章内容有助于更好的理解本文: Spring Cloud第一篇 | Spring Cloud前言及其常用组件介绍概览 Spring Clo ...
- spring cloud 入门系列七:基于Git存储的分布式配置中心
我们前面接触到的spring cloud组件都是基于Netflix的组件进行实现的,这次我们来看下spring cloud 团队自己创建的一个全新项目:Spring Cloud Config.它用来为 ...
- Spring Cloud(Dalston.SR5)--Config 集群配置中心
Spring Cloud Config 是一个全新的项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,他分为服务端和客户端两个部分.服务端也称为分布式配置中心,是一个独立的微服务 ...
- spring boot 2.0.3+spring cloud (Finchley)6、配置中心Spring Cloud Config
https://www.cnblogs.com/cralor/p/9239976.html Spring Cloud Config 是用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持, ...
- spring cloud 入门系列七:基于Git存储的分布式配置中心--Spring Cloud Config
我们前面接触到的spring cloud组件都是基于Netflix的组件进行实现的,这次我们来看下spring cloud 团队自己创建的一个全新项目:Spring Cloud Config.它用来为 ...
随机推荐
- nodejs之crypto加密算法
示例 const crypto = require('crypto'); const hash = crypto.createHash('sha256'); hash.update('some dat ...
- css固定表头,表单内容可以滑动
<html><head> <meta charset="utf-8"> <title>Table</title&g ...
- Python基础之继承与派生
一.什么是继承: 继承是一种创建新的类的方式,新建的类可以继承一个或过个父类,原始类成为基类或超类,新建的类则称为派生类 或子类. 其中,继承又分为:单继承和多继承. class parent_cla ...
- element-ui中用el-dialog+el-table+el-pagination实现文件默认选中且在分页的条件下有记忆功能
需求: 点击按钮,出现列表弹框,选择需要的选项确认后显示选中的选项.每次点击按钮,列表中默认显示之前选中的文件. el-table-column,设type属性为selection,实现表格中多行选择 ...
- 小学生都看得懂的C语言入门(5): 指针
现在已经学到C语言的后面了, 快学完咯.... (一)取地址运算 先来看一下sizeof 计算所占字节 #include<stdio.h> int main() { int a; a=; ...
- 解决npm install过程中报错:unable to verify the first certificate
今天使用npm安装开发包时遇到“unable to verify the first certificate”(无法验证第一证书)这个问题 原因:2014年2月27日,npm不再支持自签名证书.因为n ...
- easyui之自定义字体图标(鼠标覆盖时切换颜色)
项目要求是自定义字体图标,使用easyui框架结构,众所周知easyui强功能弱样式,字体图标其实就是一张图片.要达到切换图标颜色的效果,要么就是有两套图,使用js控制.但是我这个人比较懒,不喜欢做复 ...
- lisp : set 与setq 函数
在Lisp中,如果我们希望对一个变量赋值,可以使用set函数,用法如下: (set ‘my-value "my string") 上面的代码是对变量my-value进行赋值,值是& ...
- Tensorflow 中的优化器解析
Tensorflow:1.6.0 优化器(reference:https://blog.csdn.net/weixin_40170902/article/details/80092628) I: t ...
- gitlab使用--汉化及修改端口
汉化思路:去gitlab汉化社区下载对应的汉化版本,这个文件和当前版本对比形成一个补丁,打入到当前配置文件中 1.查看当前gitlab版本 head -1(数字) /opt/gitlab/ve ...