The @Value annotation is provided in Spring to bind configurations so that they can be read from the configuration file and assigned to member variables; sometimes, our configurations may not be in the configuration file, if they existDb/redis/other files/third-party configuration services, this article will teach you how to implement a custom configuration loader and support the @Value posture
<!-- more -->
I. Environment & Scheme Design
1. Environment
- SpringBoot 2.2.1.RELEASE
- IDEA + JDK8
2. Scheme Design
Custom configuration loading with two core roles
- Configuration Container MetaValHolder: Handles specific configuration and provides configuration
- Configuration Binding@MetaVal: Similar to the @Value annotation, which binds class properties to specific configurations and enables configuration initialization and refresh when configuration changes occur
Above @MetaVal mentions two things, one is initialization, the other is configuration refresh, so let's see how to support both
a. Initialization
The premise of initialization is to get all the members decorated with this annotation, then use MetaValHolder to get the corresponding configuration and initialize it
To achieve this, the best starting point is to get all the properties of the bean after the Bean object is created and see if this annotation is marked, using the InstantiationAwareBeanPostProcessorAdapter
b. Refresh
We also want the bound properties to change when the configuration changes, so we need to preserve the binding relationship between the configuration and the bean properties
Configuration changes and refresh of bean properties can be decoupled by Spring's event mechanism. When configuring changes, a MetaChangeEvent event is thrown. By default, we provide an event handler to update the bean properties bound through the @MetaVal annotation
In addition to decoupling, use events have the added benefit of being more flexible, such as supporting user extensions to the use of configurations
II. Implementation
1. MetaVal annotation
Provides a binding relationship between configuration and bean properties. Here we only provide a basic capability to get configuration based on configuration name. Interested partners can extend their support for SPEL by themselves
@Target({ElementType.FIELD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface MetaVal { /** * Get Configured Rules * * @return */ String value() default ""; /** * meta value Convert the target object; currently provides basic data type support * * @return */ MetaParser parser() default MetaParser.STRING_PARSER; }
Note that the above implementation has a parser in addition to value, because our configuration value may be a String or, of course, other basic types such as int, boolean; therefore, a basic type converter is provided
public interface IMetaParser<T> { T parse(String val); } public enum MetaParser implements IMetaParser { STRING_PARSER { @Override public String parse(String val) { return val; } }, SHORT_PARSER { @Override public Short parse(String val) { return Short.valueOf(val); } }, INT_PARSER { @Override public Integer parse(String val) { return Integer.valueOf(val); } }, LONG_PARSER { @Override public Long parse(String val) { return Long.valueOf(val); } }, FLOAT_PARSER { @Override public Object parse(String val) { return null; } }, DOUBLE_PARSER { @Override public Object parse(String val) { return Double.valueOf(val); } }, BYTE_PARSER { @Override public Byte parse(String val) { if (val == null) { return null; } return Byte.valueOf(val); } }, CHARACTER_PARSER { @Override public Character parse(String val) { if (val == null) { return null; } return val.charAt(0); } }, BOOLEAN_PARSER { @Override public Boolean parse(String val) { return Boolean.valueOf(val); } }; }
2. MetaValHolder
Provides a core class of configurations, where only one interface is defined and specific configuration acquisitions are relevant to business needs
public interface MetaValHolder { /** * Get Configuration * * @param key * @return */ String getProperty(String key); }
To support configuration refresh, we provide an abstract class based on Spring event notification mechanism
public abstract class AbstractMetaValHolder implements MetaValHolder, ApplicationContextAware { protected ApplicationContext applicationContext; public void updateProperty(String key, String value) { String old = this.doUpdateProperty(key, value); this.applicationContext.publishEvent(new MetaChangeEvent(this, key, old, value)); } /** * Update Configuration * * @param key * @param value * @return */ public abstract String doUpdateProperty(String key, String value); @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
3. MetaValueRegister Configuration Binding and Initialization
This class, which provides the ability to scan all bean s, get @MetaVal-modified properties, and initialize
public class MetaValueRegister extends InstantiationAwareBeanPostProcessorAdapter { private MetaContainer metaContainer; public MetaValueRegister(MetaContainer metaContainer) { this.metaContainer = metaContainer; } @Override public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { processMetaValue(bean); return super.postProcessAfterInstantiation(bean, beanName); } /** * Scan all properties of the bean and get @MetaVal decorated properties * @param bean */ private void processMetaValue(Object bean) { try { Class clz = bean.getClass(); MetaVal metaVal; for (Field field : clz.getDeclaredFields()) { metaVal = field.getAnnotation(MetaVal.class); if (metaVal != null) { // Cache configuration binds to Field and initializes metaContainer.addInvokeCell(metaVal, bean, field); } } } catch (Exception e) { e.printStackTrace(); System.exit(-1); } } }
Note that the core point above is metaContainer.addInvokeCell(metaVal, bean, field); this line
4. MetaContainer
Configure containers, save configuration and field mapping relationships, and provide basic operations for configuration
@Slf4j public class MetaContainer { private MetaValHolder metaValHolder; // Save binding relationship between configuration and Field private Map<String, Set<InvokeCell>> metaCache = new ConcurrentHashMap<>(); public MetaContainer(MetaValHolder metaValHolder) { this.metaValHolder = metaValHolder; } public String getProperty(String key) { return metaValHolder.getProperty(key); } // Used to add new binding relationships and initialize public void addInvokeCell(MetaVal metaVal, Object target, Field field) throws IllegalAccessException { String metaKey = metaVal.value(); if (!metaCache.containsKey(metaKey)) { synchronized (this) { if (!metaCache.containsKey(metaKey)) { metaCache.put(metaKey, new HashSet<>()); } } } metaCache.get(metaKey).add(new InvokeCell(metaVal, target, field, getProperty(metaKey))); } // Configuration updates public void updateMetaVal(String metaKey, String oldVal, String newVal) { Set<InvokeCell> cacheSet = metaCache.get(metaKey); if (CollectionUtils.isEmpty(cacheSet)) { return; } cacheSet.forEach(s -> { try { s.update(newVal); log.info("update {} from {} to {}", s.getSignature(), oldVal, newVal); } catch (IllegalAccessException e) { e.printStackTrace(); } }); } @Data public static class InvokeCell { private MetaVal metaVal; private Object target; private Field field; private String signature; private Object value; public InvokeCell(MetaVal metaVal, Object target, Field field, String value) throws IllegalAccessException { this.metaVal = metaVal; this.target = target; this.field = field; field.setAccessible(true); signature = target.getClass().getName() + "." + field.getName(); this.update(value); } public void update(String value) throws IllegalAccessException { this.value = this.metaVal.parser().parse(value); field.set(target, this.value); } } }
5. Event/Listener
Next is support for event notification mechanism
MetaChangeEvent configures change events, provides three basic information, configures key, original value, new value
@ToString @EqualsAndHashCode public class MetaChangeEvent extends ApplicationEvent { private static final long serialVersionUID = -9100039605582210577L; private String key; private String oldVal; private String newVal; /** * Create a new {@code ApplicationEvent}. * * @param source the object on which the event initially occurred or with * which the event is associated (never {@code null}) */ public MetaChangeEvent(Object source) { super(source); } public MetaChangeEvent(Object source, String key, String oldVal, String newVal) { super(source); this.key = key; this.oldVal = oldVal; this.newVal = newVal; } public String getKey() { return key; } public String getOldVal() { return oldVal; } public String getNewVal() { return newVal; } }
MetaChangeListener event handler, refresh configuration of @MetaVal binding
public class MetaChangeListener implements ApplicationListener<MetaChangeEvent> { private MetaContainer metaContainer; public MetaChangeListener(MetaContainer metaContainer) { this.metaContainer = metaContainer; } @Override public void onApplicationEvent(MetaChangeEvent event) { metaContainer.updateMetaVal(event.getKey(), event.getOldVal(), event.getNewVal()); } }
6. bean Configuration
In the last five steps, a custom configuration loader is basically finished, and the rest is the declaration of the bean
@Configuration public class DynamicConfig { @Bean @ConditionalOnMissingBean(MetaValHolder.class) public MetaValHolder metaValHolder() { return key -> null; } @Bean public MetaContainer metaContainer(MetaValHolder metaValHolder) { return new MetaContainer(metaValHolder); } @Bean public MetaValueRegister metaValueRegister(MetaContainer metaContainer) { return new MetaValueRegister(metaContainer); } @Bean public MetaChangeListener metaChangeListener(MetaContainer metaContainer) { return new MetaChangeListener(metaContainer); } }
External use is provided as a two-party toolkit, so a new file META-INF/spring.factories needs to be created in the resource directory.
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.git.hui.boot.dynamic.config.DynamicConfig
6. Testing
Complete the basic functions above, and then proceed to the testing phase to customize a configuration load
@Component public class MetaPropertyHolder extends AbstractMetaValHolder { public Map<String, String> metas = new HashMap<>(8); { metas.put("name", "One Gray"); metas.put("blog", "https://blog.hhui.top"); metas.put("age", "18"); } @Override public String getProperty(String key) { return metas.getOrDefault(key, ""); } @Override public String doUpdateProperty(String key, String value) { return metas.put(key, value); } }
A demoBean using MetaVal
@Component public class DemoBean { @MetaVal("name") private String name; @MetaVal("blog") private String blog; @MetaVal(value = "age", parser = MetaParser.INT_PARSER) private Integer age; public String sayHello() { return "Welcome to your attention [" + name + "] Blog:" + blog + " | " + age; } }
A simple REST service for viewing/updating configurations
@RestController public class DemoAction { @Autowired private DemoBean demoBean; @Autowired private MetaPropertyHolder metaPropertyHolder; @GetMapping(path = "hello") public String hello() { return demoBean.sayHello(); } @GetMapping(path = "update") public String updateBlog(@RequestParam(name = "key") String key, @RequestParam(name = "val") String val, HttpServletResponse response) throws IOException { metaPropertyHolder.updateProperty(key, val); response.sendRedirect("/hello"); return "over!"; } }
Startup Class
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }
Motion Diagram Demonstrates Configuration Acquisition and Refresh Processes
When configuring refresh, log output occurs as follows
II. Other
0. Project
Project Source
- Project: https://github.com/liuyueyi/spring-boot-demo
- Source: - https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/002-dynamic-config - https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-case/002-dynamic-config-demo
Recommended posts
- [DB Series] Use Redis for ranking functionality (application)
- [DB Series] Build a simple site statistics service with Redis (Application Paper)
- Implement backend interface version support (application)
- [WEB Series] A Sample Project of Scavenger Login (Application)
- [Basic Series] AOP implements a log plug-in (application)
- Unregistration of Bean and Dynamic Registration for Service mock (Application)
- [Basic Series] Implement a custom Bean registrar from 0 to 1 (Application)
- FactoryBean and Proxy Instances for Implementing SPI Mechanisms (Application)
- How to specify that bean s should be loaded first (Application)
- Implement a simple distributed timer task (application)
1.A grey Blog
Unlike letters, the above are purely family statements. Due to limited personal abilities, there are unavoidable omissions and errors. If bug s are found or there are better suggestions, you are welcome to criticize and correct them with gratitude.
Below is a grey personal blog, which records all the blogs in study and work. Welcome to visit it
- A Grey Blog Personal Blog https://blog.hhui.top
- A Grey Blog-Spring Thematic Blog http://spring.hhui.top