diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java new file mode 100644 index 000000000..fbbddc853 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.apollox; + +import java.util.Set; + +/** + * 配置接口 + * + * @author Jason Song(song_s@ctrip.com) + */ +public interface Config { + + /** + * Return the property value with the given key, or {@code defaultValue} if the key doesn't exist. + * + * @param key the property name + * @param defaultValue the default value when key is not found or any error occurred + * @return the property value + */ + String getProperty(String key, String defaultValue); + + /** + * Return a set of the property names + * + * @return the property names + */ + Set getPropertyNames(); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/ConfigChangeListener.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/ConfigChangeListener.java new file mode 100644 index 000000000..7b782f3ef --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/ConfigChangeListener.java @@ -0,0 +1,19 @@ +package cn.iocoder.dashboard.framework.apollox; + +import cn.iocoder.dashboard.framework.apollox.model.ConfigChangeEvent; + +/** + * {@link Config} 变化监听器 + * + * @author Jason Song(song_s@ctrip.com) + */ +public interface ConfigChangeListener { + + /** + * Invoked when there is any config change for the namespace. + * + * @param changeEvent the event for this change + */ + void onChange(ConfigChangeEvent changeEvent); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/DBConfig.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/DBConfig.java new file mode 100644 index 000000000..9ad3289ad --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/DBConfig.java @@ -0,0 +1,17 @@ +package cn.iocoder.dashboard.framework.apollox; + +import java.util.Set; + +public class DBConfig implements Config { + + @Override + public String getProperty(String key, String defaultValue) { + return null; + } + + @Override + public Set getPropertyNames() { + return null; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/enums/PropertyChangeType.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/enums/PropertyChangeType.java new file mode 100644 index 000000000..10681b894 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/enums/PropertyChangeType.java @@ -0,0 +1,14 @@ +package cn.iocoder.dashboard.framework.apollox.enums; + +/** + * 属性变化类型枚举 + * + * @author Jason Song(song_s@ctrip.com) + */ +public enum PropertyChangeType { + + ADDED, // 添加 + MODIFIED, // 修改 + DELETED // 删除 + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/model/ConfigChange.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/model/ConfigChange.java new file mode 100644 index 000000000..ac52f10be --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/model/ConfigChange.java @@ -0,0 +1,35 @@ +package cn.iocoder.dashboard.framework.apollox.model; + + +import cn.iocoder.dashboard.framework.apollox.enums.PropertyChangeType; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Holds the information for a config change. + * 配置每个属性变化的信息 + * + * @author Jason Song(song_s@ctrip.com) + */ +@Data +@AllArgsConstructor +public class ConfigChange { + + /** + * 属性名 + */ + private final String propertyName; + /** + * 老值 + */ + private String oldValue; + /** + * 新值 + */ + private String newValue; + /** + * 变化类型 + */ + private PropertyChangeType changeType; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/model/ConfigChangeEvent.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/model/ConfigChangeEvent.java new file mode 100644 index 000000000..50aaab566 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/model/ConfigChangeEvent.java @@ -0,0 +1,53 @@ +package cn.iocoder.dashboard.framework.apollox.model; + +import lombok.AllArgsConstructor; + +import java.util.Map; +import java.util.Set; + +/** + * A change event when a namespace's config is changed. + * + * @author Jason Song(song_s@ctrip.com) + */ +@AllArgsConstructor +public class ConfigChangeEvent { + + /** + * 变化属性的集合 + * + * KEY:属性名 + * VALUE:配置变化 + */ + private final Map m_changes; + + /** + * Get the keys changed. + * + * @return the list of the keys + */ + public Set changedKeys() { + return m_changes.keySet(); + } + + /** + * Get a specific change instance for the key specified. + * + * @param key the changed key + * @return the change instance + */ + public ConfigChange getChange(String key) { + return m_changes.get(key); + } + + /** + * Check whether the specified key is changed + * + * @param key the key + * @return true if the key is changed, false otherwise. + */ + public boolean isChanged(String key) { + return m_changes.containsKey(key); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/package-info.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/package-info.java new file mode 100644 index 000000000..7e4350f55 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/package-info.java @@ -0,0 +1,16 @@ +/** + * 配置中心客户端,基于 Apollo Client 实现,所以叫 ApolloX + * + * 差别在于,我们使用 {@link cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.config.SysConfigDO} 表作为配置源。 + * 当然,功能肯定也会相对少些,满足最小化诉求。 + * + * 1. 项目初始化时,可以使用 SysConfigDO 表的配置 + * 2. 使用 Spring @Value 可以注入属性 + * 3. SysConfigDO 表的配置修改时,注入到 @Value 的属性可以刷新 + * + * 另外,整个包结构会参考 Apollo 为主,方便维护与理解 + * + * 注意,目前有两个特性是不支持的 + * 1. 自定义配置变化的监听器 + */ +package cn.iocoder.dashboard.framework.apollox; diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloProcessor.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloProcessor.java new file mode 100644 index 000000000..29d222c3c --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloProcessor.java @@ -0,0 +1,68 @@ +package cn.iocoder.dashboard.framework.apollox.spring.annotation; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; + +/** + * Apollo 处理器抽象类,封装了在 Spring Bean 初始化之前,处理属性和方法。 + * + * Create by zhangzheng on 2018/2/6 + */ +public abstract class ApolloProcessor implements BeanPostProcessor, PriorityOrdered { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + Class clazz = bean.getClass(); + // 处理所有 Field + for (Field field : findAllField(clazz)) { + processField(bean, beanName, field); + } + // 处理所有的 Method + for (Method method : findAllMethod(clazz)) { + processMethod(bean, beanName, method); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + /** + * subclass should implement this method to process field + */ + protected abstract void processField(Object bean, String beanName, Field field); + + /** + * subclass should implement this method to process method + */ + protected abstract void processMethod(Object bean, String beanName, Method method); + + @Override + public int getOrder() { + // make it as late as possible + return Ordered.LOWEST_PRECEDENCE; // 最高优先级 + } + + private List findAllField(Class clazz) { + final List res = new LinkedList<>(); + ReflectionUtils.doWithFields(clazz, res::add); + return res; + } + + private List findAllMethod(Class clazz) { + final List res = new LinkedList<>(); + ReflectionUtils.doWithMethods(clazz, res::add); + return res; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/SpringValueProcessor.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/SpringValueProcessor.java new file mode 100644 index 000000000..f5cc34060 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/SpringValueProcessor.java @@ -0,0 +1,141 @@ +package cn.iocoder.dashboard.framework.apollox.spring.annotation; + +import cn.hutool.core.lang.Singleton; +import cn.iocoder.dashboard.framework.apollox.spring.property.*; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.Bean; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Set; + +/** + * Spring value processor of field or method which has @Value and xml config placeholders. + * + * Spring Value 处理器,处理: + * + * 1. 带有 `@Value` 注解的 Field 和 Method + * 2. XML 配置的 Bean 的 PlaceHolder 们 + * + * 每个 Field、Method、XML PlaceHolder 被处理成一个 SpringValue 对象,添加到 SpringValueRegistry 中。 + * + * 目的还是,为了 PlaceHolder 的自动更新机制。 + * + * @author github.com/zhegexiaohuozi seimimaster@gmail.com + * @since 2017/12/20. + */ +@Slf4j +public class SpringValueProcessor extends ApolloProcessor implements BeanFactoryPostProcessor { + + /** + * SpringValueDefinition 集合 + * + * KEY:beanName + * VALUE:SpringValueDefinition 集合 + */ + private static Multimap beanName2SpringValueDefinitions = LinkedListMultimap.create(); + + private final PlaceholderHelper placeholderHelper = Singleton.get(PlaceholderHelper.class); + private final SpringValueRegistry springValueRegistry = Singleton.get(SpringValueRegistry.class); + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + beanName2SpringValueDefinitions = SpringValueDefinitionProcessor.getBeanName2SpringValueDefinitions(); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + // 处理 Field 和 Method + super.postProcessBeforeInitialization(bean, beanName); + // 处理 XML 配置的 Bean 的 PlaceHolder 们 + processBeanPropertyValues(bean, beanName); + return bean; + } + + @Override + protected void processField(Object bean, String beanName, Field field) { + // register @Value on field + Value value = field.getAnnotation(Value.class); + if (value == null) { + return; + } + // 提取 `keys` 属性们。 + Set keys = placeholderHelper.extractPlaceholderKeys(value.value()); + if (keys.isEmpty()) { + return; + } + // 循环 `keys` ,创建对应的 SpringValue 对象,并添加到 `springValueRegistry` 中。 + for (String key : keys) { + SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false); + springValueRegistry.register(key, springValue); + log.debug("Monitoring {}", springValue); + } + } + + @Override + protected void processMethod(Object bean, String beanName, Method method) { + // register @Value on method + Value value = method.getAnnotation(Value.class); + if (value == null) { + return; + } + // 忽略 @Bean 注解的方法 + // skip Configuration bean methods + if (method.getAnnotation(Bean.class) != null) { + return; + } + // 忽略非 setting 方法 + if (method.getParameterTypes().length != 1) { + log.error("Ignore @Value setter {}.{}, expecting 1 parameter, actual {} parameters", bean.getClass().getName(), method.getName(), method.getParameterTypes().length); + return; + } + // 提取 `keys` 属性们。 + Set keys = placeholderHelper.extractPlaceholderKeys(value.value()); + if (keys.isEmpty()) { + return; + } + // 循环 `keys` ,创建对应的 SpringValue 对象,并添加到 `springValueRegistry` 中。 + for (String key : keys) { + SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, method, false); + springValueRegistry.register(key, springValue); + log.info("Monitoring {}", springValue); + } + } + + private void processBeanPropertyValues(Object bean, String beanName) { + // 获得 SpringValueDefinition 数组 + Collection propertySpringValues = beanName2SpringValueDefinitions.get(beanName); + if (propertySpringValues == null || propertySpringValues.isEmpty()) { + return; + } + // 循环 SpringValueDefinition 数组,创建对应的 SpringValue 对象,并添加到 `springValueRegistry` 中。 + for (SpringValueDefinition definition : propertySpringValues) { + try { + PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(bean.getClass(), definition.getPropertyName()); + Method method = pd.getWriteMethod(); + if (method == null) { + continue; + } + SpringValue springValue = new SpringValue(definition.getKey(), definition.getPlaceholder(), bean, beanName, method, false); + springValueRegistry.register(definition.getKey(), springValue); + log.debug("Monitoring {}", springValue); + } catch (Throwable ex) { + log.error("Failed to enable auto update feature for {}.{}", bean.getClass(), definition.getPropertyName()); + } + } + + // clear + // 移除 Bean 对应的 SpringValueDefinition 数组 + beanName2SpringValueDefinitions.removeAll(beanName); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloApplicationContextInitializer.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloApplicationContextInitializer.java new file mode 100644 index 000000000..ae88e77f9 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloApplicationContextInitializer.java @@ -0,0 +1,34 @@ +package cn.iocoder.dashboard.framework.apollox.spring.boot; + + +import cn.hutool.core.lang.Singleton; +import cn.iocoder.dashboard.framework.apollox.Config; +import cn.iocoder.dashboard.framework.apollox.DBConfig; +import cn.iocoder.dashboard.framework.apollox.spring.config.ConfigPropertySource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import static cn.iocoder.dashboard.framework.apollox.spring.config.PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME; + +@Slf4j +public class ApolloApplicationContextInitializer implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext context) { + ConfigurableEnvironment environment = context.getEnvironment(); + // 忽略,若已经有 APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME 的 PropertySource + if (environment.getPropertySources().contains(APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + // already initialized + return; + } + + // 创建自定义的 APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME 的 PropertySource + Config config = Singleton.get(DBConfig.class); + ConfigPropertySource configPropertySource = new ConfigPropertySource(APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, config); + // 添加到 `environment` 中,且优先级最高 + environment.getPropertySources().addFirst(configPropertySource); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloAutoConfiguration.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloAutoConfiguration.java new file mode 100644 index 000000000..fb13b1d73 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloAutoConfiguration.java @@ -0,0 +1,18 @@ +package cn.iocoder.dashboard.framework.apollox.spring.boot; + +import cn.iocoder.dashboard.framework.apollox.spring.config.ConfigPropertySourcesProcessor; +import cn.iocoder.dashboard.framework.apollox.spring.property.PropertySourcesProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnMissingBean(PropertySourcesProcessor.class) // 缺失 PropertySourcesProcessor 时 +public class ApolloAutoConfiguration { + + @Bean + public ConfigPropertySourcesProcessor configPropertySourcesProcessor() { + return new ConfigPropertySourcesProcessor(); // 注入 ConfigPropertySourcesProcessor bean 对象 + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySource.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySource.java new file mode 100644 index 000000000..8d0f4d41d --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySource.java @@ -0,0 +1,39 @@ +package cn.iocoder.dashboard.framework.apollox.spring.config; + +import cn.iocoder.dashboard.framework.apollox.Config; +import org.springframework.core.env.EnumerablePropertySource; + +import java.util.Set; + +/** + * Property source wrapper for Config + * + * 基于 {@link Config} 的 PropertySource 实现类 + * + * @author Jason Song(song_s@ctrip.com) + */ +public class ConfigPropertySource extends EnumerablePropertySource { + + private static final String[] EMPTY_ARRAY = new String[0]; + + public ConfigPropertySource(String name, Config source) { // 此处的 Apollo Config 作为 `source` + super(name, source); + } + + @Override + public String[] getPropertyNames() { + // 从 Config 中,获得属性名集合 + Set propertyNames = this.source.getPropertyNames(); + // 转换成 String 数组,返回 + if (propertyNames.isEmpty()) { + return EMPTY_ARRAY; + } + return propertyNames.toArray(new String[0]); + } + + @Override + public Object getProperty(String name) { + return this.source.getProperty(name, null); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourcesProcessor.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourcesProcessor.java new file mode 100644 index 000000000..bcf205648 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourcesProcessor.java @@ -0,0 +1,42 @@ +package cn.iocoder.dashboard.framework.apollox.spring.config; + +import cn.iocoder.dashboard.framework.apollox.spring.annotation.SpringValueProcessor; +import cn.iocoder.dashboard.framework.apollox.spring.property.PropertySourcesProcessor; +import cn.iocoder.dashboard.framework.apollox.spring.property.SpringValueDefinitionProcessor; +import cn.iocoder.dashboard.framework.apollox.spring.util.BeanRegistrationUtil; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; + +/** + * Apollo Property Sources processor for Spring XML Based Application + * + * @author Jason Song(song_s@ctrip.com) + */ +public class ConfigPropertySourcesProcessor extends PropertySourcesProcessor implements BeanDefinitionRegistryPostProcessor { + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + // 注册 PropertySourcesPlaceholderConfigurer 到 BeanDefinitionRegistry 中,替换 PlaceHolder 为对应的属性值,参考文章 https://leokongwq.github.io/2016/12/28/spring-PropertyPlaceholderConfigurer.html + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(), PropertySourcesPlaceholderConfigurer.class); + // 注册 SpringValueProcessor 到 BeanDefinitionRegistry 中,用于 PlaceHolder 自动更新机制 + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), SpringValueProcessor.class); + + // 处理 XML 配置的 Spring PlaceHolder + processSpringValueDefinition(registry); + } + + /** + * For Spring 3.x versions, the BeanDefinitionRegistryPostProcessor would not be + * instantiated if it is added in postProcessBeanDefinitionRegistry phase, so we have to manually + * call the postProcessBeanDefinitionRegistry method of SpringValueDefinitionProcessor here... + */ + private void processSpringValueDefinition(BeanDefinitionRegistry registry) { + // 创建 SpringValueDefinitionProcessor 对象 + SpringValueDefinitionProcessor springValueDefinitionProcessor = new SpringValueDefinitionProcessor(); + // 处理 XML 配置的 Spring PlaceHolder + springValueDefinitionProcessor.postProcessBeanDefinitionRegistry(registry); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/PropertySourcesConstants.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/PropertySourcesConstants.java new file mode 100644 index 000000000..5cc048f97 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/PropertySourcesConstants.java @@ -0,0 +1,7 @@ +package cn.iocoder.dashboard.framework.apollox.spring.config; + +public interface PropertySourcesConstants { + + String APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME = "ApolloBootstrapPropertySources"; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/AutoUpdateConfigChangeListener.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/AutoUpdateConfigChangeListener.java new file mode 100644 index 000000000..05dcd0e10 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/AutoUpdateConfigChangeListener.java @@ -0,0 +1,159 @@ +package cn.iocoder.dashboard.framework.apollox.spring.property; + +import cn.hutool.core.lang.Singleton; +import cn.iocoder.dashboard.framework.apollox.ConfigChangeListener; +import cn.iocoder.dashboard.framework.apollox.enums.PropertyChangeType; +import cn.iocoder.dashboard.framework.apollox.model.ConfigChange; +import cn.iocoder.dashboard.framework.apollox.model.ConfigChangeEvent; +import com.alibaba.fastjson.JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.env.Environment; +import org.springframework.util.CollectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; + +/** + * 自动更新配置监听器 + * + * Create by zhangzheng on 2018/3/6 + */ +public class AutoUpdateConfigChangeListener implements ConfigChangeListener { + + private static final Logger logger = LoggerFactory.getLogger(AutoUpdateConfigChangeListener.class); + + /** + * {@link TypeConverter#convertIfNecessary(Object, Class, Field)} 是否带上 Field 参数,因为 Spring 3.2.0+ 才有该方法 + */ + private final boolean typeConverterHasConvertIfNecessaryWithFieldParameter; + private final Environment environment; + private final ConfigurableBeanFactory beanFactory; + /** + * TypeConverter 对象,参见 https://blog.csdn.net/rulerp2014/article/details/51100857 + */ + private final TypeConverter typeConverter; + private final PlaceholderHelper placeholderHelper; + private final SpringValueRegistry springValueRegistry; + + public AutoUpdateConfigChangeListener(Environment environment, ConfigurableListableBeanFactory beanFactory) { + this.typeConverterHasConvertIfNecessaryWithFieldParameter = testTypeConverterHasConvertIfNecessaryWithFieldParameter(); + this.beanFactory = beanFactory; + this.typeConverter = this.beanFactory.getTypeConverter(); + this.environment = environment; + this.placeholderHelper = Singleton.get(PlaceholderHelper.class); + this.springValueRegistry = Singleton.get(SpringValueRegistry.class); + } + + @Override + public void onChange(ConfigChangeEvent changeEvent) { + // 获得更新的 KEY 集合 + Set keys = changeEvent.changedKeys(); + if (CollectionUtils.isEmpty(keys)) { + return; + } + // 循环 KEY 集合,更新 StringValue + for (String key : keys) { + // 忽略,若不在 SpringValueRegistry 中 + // 1. check whether the changed key is relevant + Collection targetValues = springValueRegistry.get(key); + if (targetValues == null || targetValues.isEmpty()) { + continue; + } + // 校验是否需要更新 + // 2. check whether the value is really changed or not (since spring property sources have hierarchies) + if (!shouldTriggerAutoUpdate(changeEvent, key)) { + continue; + } + // 循环,更新 SpringValue + // 3. update the value + for (SpringValue val : targetValues) { + updateSpringValue(val); + } + } + } + + /** + * Check whether we should trigger the auto update or not. + *
+ * For added or modified keys, we should trigger auto update if the current value in Spring equals to the new value. + *
+ * For deleted keys, we will trigger auto update anyway. + */ + private boolean shouldTriggerAutoUpdate(ConfigChangeEvent changeEvent, String changedKey) { + ConfigChange configChange = changeEvent.getChange(changedKey); + // 若变更类型为删除,需要触发更新 + if (configChange.getChangeType() == PropertyChangeType.DELETED) { + return true; + } + // 若变更类型为新增或修改,判断 environment 的值是否和最新值相等。 + // 【高能】!!! + return Objects.equals(environment.getProperty(changedKey), configChange.getNewValue()); + } + + private void updateSpringValue(SpringValue springValue) { + try { + // 解析值 + Object value = resolvePropertyValue(springValue); + // 更新 StringValue + springValue.update(value); + logger.info("Auto update apollo changed value successfully, new value: {}, {}", value, springValue); + } catch (Throwable ex) { + logger.error("Auto update apollo changed value failed, {}", springValue.toString(), ex); + } + } + + /** + * Logic transplanted from DefaultListableBeanFactory + * + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency(org.springframework.beans.factory.config.DependencyDescriptor, String, Set, TypeConverter) + */ + private Object resolvePropertyValue(SpringValue springValue) { + // value will never be null, as @Value and @ApolloJsonValue will not allow that + Object value = placeholderHelper.resolvePropertyValue(beanFactory, springValue.getBeanName(), springValue.getPlaceholder()); + // 如果值数据结构是 JSON 类型,则使用 Gson 解析成对应值的类型 + if (springValue.isJson()) { + value = parseJsonValue((String) value, springValue.getGenericType()); + } else { + // 如果类型为 Field + if (springValue.isField()) { + // org.springframework.beans.TypeConverter#convertIfNecessary(java.lang.Object, java.lang.Class, java.lang.reflect.Field) is available from Spring 3.2.0+ + if (typeConverterHasConvertIfNecessaryWithFieldParameter) { + value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType(), springValue.getField()); + } else { + value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType()); + } + // 如果类型为 Method + } else { + value = this.typeConverter.convertIfNecessary(value, springValue.getTargetType(), springValue.getMethodParameter()); + } + } + + return value; + } + + private Object parseJsonValue(String json, Type targetType) { + try { + return JSON.parseObject(json, targetType); + } catch (Throwable ex) { + logger.error("Parsing json '{}' to type {} failed!", json, targetType, ex); + throw ex; + } + } + + private boolean testTypeConverterHasConvertIfNecessaryWithFieldParameter() { + try { + TypeConverter.class.getMethod("convertIfNecessary", Object.class, Class.class, Field.class); + } catch (Throwable ex) { + return false; + } + return true; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PlaceholderHelper.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PlaceholderHelper.java new file mode 100644 index 000000000..2bd5fed4f --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PlaceholderHelper.java @@ -0,0 +1,160 @@ +package cn.iocoder.dashboard.framework.apollox.spring.property; + +import com.google.common.base.Strings; +import com.google.common.collect.Sets; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.util.StringUtils; + +import java.util.Set; +import java.util.Stack; + +/** + * Placeholder 工具类 + * + * Placeholder helper functions. + */ +public class PlaceholderHelper { + + private static final String PLACEHOLDER_PREFIX = "${"; + private static final String PLACEHOLDER_SUFFIX = "}"; + private static final String VALUE_SEPARATOR = ":"; + private static final String SIMPLE_PLACEHOLDER_PREFIX = "{"; + private static final String EXPRESSION_PREFIX = "#{"; + private static final String EXPRESSION_SUFFIX = "}"; + + /** + * Resolve placeholder property values, e.g. + * + * "${somePropertyValue}" -> "the actual property value" + */ + public Object resolvePropertyValue(ConfigurableBeanFactory beanFactory, String beanName, String placeholder) { + // resolve string value + String strVal = beanFactory.resolveEmbeddedValue(placeholder); + // 获得 BeanDefinition 对象 + BeanDefinition bd = (beanFactory.containsBean(beanName) ? beanFactory.getMergedBeanDefinition(beanName) : null); + // resolve expressions like "#{systemProperties.myProp}" + return evaluateBeanDefinitionString(beanFactory, strVal, bd); + } + + private Object evaluateBeanDefinitionString(ConfigurableBeanFactory beanFactory, String value, BeanDefinition beanDefinition) { + if (beanFactory.getBeanExpressionResolver() == null) { + return value; + } + Scope scope = (beanDefinition != null ? beanFactory.getRegisteredScope(beanDefinition.getScope()) : null); + return beanFactory.getBeanExpressionResolver().evaluate(value, new BeanExpressionContext(beanFactory, scope)); + } + + /** + * Extract keys from placeholder, e.g. + *
    + *
  • ${some.key} => "some.key"
  • + *
  • ${some.key:${some.other.key:100}} => "some.key", "some.other.key"
  • + *
  • ${${some.key}} => "some.key"
  • + *
  • ${${some.key:other.key}} => "some.key"
  • + *
  • ${${some.key}:${another.key}} => "some.key", "another.key"
  • + *
  • #{new java.text.SimpleDateFormat('${some.key}').parse('${another.key}')} => "some.key", "another.key"
  • + *
+ */ + public Set extractPlaceholderKeys(String propertyString) { + Set placeholderKeys = Sets.newHashSet(); + + if (!isNormalizedPlaceholder(propertyString) && !isExpressionWithPlaceholder(propertyString)) { + return placeholderKeys; + } + + Stack stack = new Stack<>(); + stack.push(propertyString); + + while (!stack.isEmpty()) { + String strVal = stack.pop(); + int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX); + if (startIndex == -1) { + placeholderKeys.add(strVal); + continue; + } + int endIndex = findPlaceholderEndIndex(strVal, startIndex); + if (endIndex == -1) { + // invalid placeholder? + continue; + } + + String placeholderCandidate = strVal.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex); + + // ${some.key:other.key} + if (placeholderCandidate.startsWith(PLACEHOLDER_PREFIX)) { + stack.push(placeholderCandidate); + } else { + // some.key:${some.other.key:100} + int separatorIndex = placeholderCandidate.indexOf(VALUE_SEPARATOR); + + if (separatorIndex == -1) { + stack.push(placeholderCandidate); + } else { + stack.push(placeholderCandidate.substring(0, separatorIndex)); + String defaultValuePart = + normalizeToPlaceholder(placeholderCandidate.substring(separatorIndex + VALUE_SEPARATOR.length())); + if (!Strings.isNullOrEmpty(defaultValuePart)) { + stack.push(defaultValuePart); + } + } + } + + // has remaining part, e.g. ${a}.${b} + if (endIndex + PLACEHOLDER_SUFFIX.length() < strVal.length() - 1) { + String remainingPart = normalizeToPlaceholder(strVal.substring(endIndex + PLACEHOLDER_SUFFIX.length())); + if (!Strings.isNullOrEmpty(remainingPart)) { + stack.push(remainingPart); + } + } + } + + return placeholderKeys; + } + + private boolean isNormalizedPlaceholder(String propertyString) { + return propertyString.startsWith(PLACEHOLDER_PREFIX) && propertyString.endsWith(PLACEHOLDER_SUFFIX); + } + + private boolean isExpressionWithPlaceholder(String propertyString) { + return propertyString.startsWith(EXPRESSION_PREFIX) && propertyString.endsWith(EXPRESSION_SUFFIX) + && propertyString.contains(PLACEHOLDER_PREFIX); + } + + private String normalizeToPlaceholder(String strVal) { + int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX); + if (startIndex == -1) { + return null; + } + int endIndex = strVal.lastIndexOf(PLACEHOLDER_SUFFIX); + if (endIndex == -1) { + return null; + } + + return strVal.substring(startIndex, endIndex + PLACEHOLDER_SUFFIX.length()); + } + + private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + PLACEHOLDER_PREFIX.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (StringUtils.substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + PLACEHOLDER_SUFFIX.length(); + } else { + return index; + } + } else if (StringUtils.substringMatch(buf, index, SIMPLE_PLACEHOLDER_PREFIX)) { + withinNestedPlaceholder++; + index = index + SIMPLE_PLACEHOLDER_PREFIX.length(); + } else { + index++; + } + } + return -1; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PropertySourcesProcessor.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PropertySourcesProcessor.java new file mode 100644 index 000000000..d3c7c77df --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PropertySourcesProcessor.java @@ -0,0 +1,53 @@ +package cn.iocoder.dashboard.framework.apollox.spring.property; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Apollo Property Sources processor for Spring Annotation Based Application.

+ *

+ * The reason why PropertySourcesProcessor implements {@link BeanFactoryPostProcessor} instead of + * {@link org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor} is that lower versions of + * Spring (e.g. 3.1.1) doesn't support registering BeanDefinitionRegistryPostProcessor in ImportBeanDefinitionRegistrar + * - {@link com.ctrip.framework.apollo.spring.annotation.ApolloConfigRegistrar} + * + * @author Jason Song(song_s@ctrip.com) + */ +public class PropertySourcesProcessor implements BeanFactoryPostProcessor, EnvironmentAware, PriorityOrdered { + + /** + * 是否初始化的标识 + */ + private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); + + /** + * Spring ConfigurableEnvironment 对象 + */ + private ConfigurableEnvironment environment; + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + } + + @Override + public void setEnvironment(Environment environment) { + //it is safe enough to cast as all known environment is derived from ConfigurableEnvironment + this.environment = (ConfigurableEnvironment) environment; + } + + @Override + public int getOrder() { + // make it as early as possible + return Ordered.HIGHEST_PRECEDENCE; // 最高优先级 + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValue.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValue.java new file mode 100644 index 000000000..0d96c100b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValue.java @@ -0,0 +1,152 @@ +package cn.iocoder.dashboard.framework.apollox.spring.property; + +import org.springframework.core.MethodParameter; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; + +/** + * Spring @Value method info + * + * @author github.com/zhegexiaohuozi seimimaster@gmail.com + * @since 2018/2/6. + */ +public class SpringValue { + + /** + * Bean 对象 + */ + private Object bean; + /** + * Bean 名字 + */ + private String beanName; + /** + * Spring 方法参数封装 + */ + private MethodParameter methodParameter; + /** + * Field + */ + private Field field; + /** + * KEY + * + * 即在 Config 中的属性 KEY 。 + */ + private String key; + /** + * 占位符 + */ + private String placeholder; + /** + * 值类型 + */ + private Class targetType; + /** + * 是否 JSON + */ + private boolean isJson; + /** + * 泛型。当是 JSON 类型时,使用 + */ + private Type genericType; + + // Field + public SpringValue(String key, String placeholder, Object bean, String beanName, Field field, boolean isJson) { + this.bean = bean; + this.beanName = beanName; + // Field + this.field = field; + this.key = key; + this.placeholder = placeholder; + // Field 差异 + this.targetType = field.getType(); + this.isJson = isJson; + if (isJson) { + this.genericType = field.getGenericType(); + } + } + + // Method + public SpringValue(String key, String placeholder, Object bean, String beanName, Method method, boolean isJson) { + this.bean = bean; + this.beanName = beanName; + // Method + this.methodParameter = new MethodParameter(method, 0); + this.key = key; + this.placeholder = placeholder; + // Method 差异 + Class[] paramTps = method.getParameterTypes(); + this.targetType = paramTps[0]; + this.isJson = isJson; + if (isJson) { + this.genericType = method.getGenericParameterTypes()[0]; + } + } + + public void update(Object newVal) throws IllegalAccessException, InvocationTargetException { + // Field + if (isField()) { + injectField(newVal); + // Method + } else { + injectMethod(newVal); + } + } + + private void injectField(Object newVal) throws IllegalAccessException { + boolean accessible = field.isAccessible(); + field.setAccessible(true); + field.set(bean, newVal); + field.setAccessible(accessible); + } + + private void injectMethod(Object newVal) throws InvocationTargetException, IllegalAccessException { + methodParameter.getMethod().invoke(bean, newVal); + } + + public String getBeanName() { + return beanName; + } + + public Class getTargetType() { + return targetType; + } + + public String getPlaceholder() { + return this.placeholder; + } + + public MethodParameter getMethodParameter() { + return methodParameter; + } + + public boolean isField() { + return this.field != null; + } + + public Field getField() { + return field; + } + + public Type getGenericType() { + return genericType; + } + + public boolean isJson() { + return isJson; + } + + @Override + public String toString() { + if (isField()) { + return String.format("key: %s, beanName: %s, field: %s.%s", key, beanName, bean.getClass().getName(), field.getName()); + } + return String.format("key: %s, beanName: %s, method: %s.%s", key, beanName, bean.getClass().getName(), + methodParameter.getMethod().getName()); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueDefinition.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueDefinition.java new file mode 100644 index 000000000..34ad40f61 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueDefinition.java @@ -0,0 +1,28 @@ +package cn.iocoder.dashboard.framework.apollox.spring.property; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Spring Value 定义 + */ +@Getter +@AllArgsConstructor +public class SpringValueDefinition { + + /** + * KEY + * + * 即在 Config 中的属性 KEY 。 + */ + private final String key; + /** + * 占位符 + */ + private final String placeholder; + /** + * 属性名 + */ + private final String propertyName; + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueDefinitionProcessor.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueDefinitionProcessor.java new file mode 100644 index 000000000..a9cf1286b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueDefinitionProcessor.java @@ -0,0 +1,94 @@ +package cn.iocoder.dashboard.framework.apollox.spring.property; + +import cn.hutool.core.lang.Singleton; +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * To process xml config placeholders, e.g. + * + *

+ *  <bean class="com.ctrip.framework.apollo.demo.spring.xmlConfigDemo.bean.XmlBean">
+ *    <property name="timeout" value="${timeout:200}"/>
+ *    <property name="batch" value="${batch:100}"/>
+ *  </bean>
+ * 
+ */ +public class SpringValueDefinitionProcessor implements BeanDefinitionRegistryPostProcessor { + + /** + * SpringValueDefinition 集合 + *

+ * KEY:beanName + * VALUE:SpringValueDefinition 集合 + */ + private static final Multimap beanName2SpringValueDefinitions = LinkedListMultimap.create(); + /** + * 是否初始化的标识 + */ + private static final AtomicBoolean initialized = new AtomicBoolean(false); + + private final PlaceholderHelper placeholderHelper = Singleton.get(PlaceholderHelper.class); + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + processPropertyValues(registry); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + + public static Multimap getBeanName2SpringValueDefinitions() { + return beanName2SpringValueDefinitions; + } + + private void processPropertyValues(BeanDefinitionRegistry beanRegistry) { + // 若已经初始化,直接返回 + if (!initialized.compareAndSet(false, true)) { + // already initialized + return; + } + // 循环 BeanDefinition 集合 + String[] beanNames = beanRegistry.getBeanDefinitionNames(); + for (String beanName : beanNames) { + BeanDefinition beanDefinition = beanRegistry.getBeanDefinition(beanName); + // 循环 BeanDefinition 的 PropertyValue 数组 + MutablePropertyValues mutablePropertyValues = beanDefinition.getPropertyValues(); + List propertyValues = mutablePropertyValues.getPropertyValueList(); + for (PropertyValue propertyValue : propertyValues) { + // 获得 `value` 属性。 + Object value = propertyValue.getValue(); + // 忽略非 Spring PlaceHolder 的 `value` 属性。 + if (!(value instanceof TypedStringValue)) { + continue; + } + // 获得 `placeholder` 属性。 + String placeholder = ((TypedStringValue) value).getValue(); + // 提取 `keys` 属性们。 + Set keys = placeholderHelper.extractPlaceholderKeys(placeholder); + if (keys.isEmpty()) { + continue; + } + // 循环 `keys` ,创建对应的 SpringValueDefinition 对象,并添加到 `beanName2SpringValueDefinitions` 中。 + for (String key : keys) { + beanName2SpringValueDefinitions.put(beanName, + new SpringValueDefinition(key, placeholder, propertyValue.getName())); + } + } + } + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueRegistry.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueRegistry.java new file mode 100644 index 000000000..c2a4bbe1b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/SpringValueRegistry.java @@ -0,0 +1,31 @@ +package cn.iocoder.dashboard.framework.apollox.spring.property; + +import com.google.common.collect.LinkedListMultimap; +import com.google.common.collect.Multimap; + +import java.util.Collection; + +/** + * {@link SpringValue} 注册表 + */ +public class SpringValueRegistry { + + /** + * SpringValue 集合 + * + * KEY:属性 KEY ,即 Config 配置 KEY + * VALUE:SpringValue 数组 + */ + private final Multimap registry = LinkedListMultimap.create(); + + // 注册 + public void register(String key, SpringValue springValue) { + registry.put(key, springValue); + } + + // 获得 + public Collection get(String key) { + return registry.get(key); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/util/BeanRegistrationUtil.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/util/BeanRegistrationUtil.java new file mode 100644 index 000000000..8e1689cd7 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/util/BeanRegistrationUtil.java @@ -0,0 +1,38 @@ +package cn.iocoder.dashboard.framework.apollox.spring.util; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; + +import java.util.Objects; + +/** + * Bean Registration 工具类 + * + * @author Jason Song(song_s@ctrip.com) + */ +public class BeanRegistrationUtil { + + // 注册 `beanClass` 到 BeanDefinitionRegistry 中,当且仅当 `beanName` 和 `beanClass` 都不存在对应的 BeanDefinition 时 + public static boolean registerBeanDefinitionIfNotExists(BeanDefinitionRegistry registry, String beanName, Class beanClass) { + // 不存在 `beanName` 对应的 BeanDefinition + if (registry.containsBeanDefinition(beanName)) { + return false; + } + + // 不存在 `beanClass` 对应的 BeanDefinition + String[] candidates = registry.getBeanDefinitionNames(); + for (String candidate : candidates) { + BeanDefinition beanDefinition = registry.getBeanDefinition(candidate); + if (Objects.equals(beanDefinition.getBeanClassName(), beanClass.getName())) { + return false; + } + } + + // 注册 `beanClass` 到 BeanDefinitionRegistry 中 + BeanDefinition annotationProcessor = BeanDefinitionBuilder.genericBeanDefinition(beanClass).getBeanDefinition(); + registry.registerBeanDefinition(beanName, annotationProcessor); + return true; + } + +} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..f70022113 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.dashboard.framework.apollox.spring.boot.ApolloAutoConfiguration +org.springframework.context.ApplicationContextInitializer=\ + cn.iocoder.dashboard.framework.apollox.spring.boot.ApolloApplicationContextInitializer