[SpringBoot Basic Series] Implement a custom configuration loader (application)

Posted by McManCSU on Thu, 07 May 2020 04:00:28 +0200

[SpringBoot Basic Series] Implement a custom configuration loader (application)

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

Recommended posts

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

Topics: Programming Spring Redis github SpringBoot