[springboot basic series] implement a custom configuration loader (application)

Time:2020-10-17

[springboot basic series] implement a custom configuration loader (application)

[springboot basic series] implement a custom configuration loader (application)

Spring provides@ValueAnnotation, which is used to bind the configuration, can read the corresponding configuration from the configuration file and assign it to the member variable. Sometimes, our configuration may not be in the configuration file. If there is dB / redis / other files / third-party configuration service, this paper will teach you to implement a custom configuration loader and support it@ValueUsing posture of

<!– more –>

1. Environment & scheme design

1. Environment

  • SpringBoot 2.2.1.RELEASE
  • IDEA + JDK8

2. Scheme design

Custom configuration loading has two core roles

  • Configuration containerMetaValHolder: dealing with specific configuration and providing configuration
  • Configure binding@MetaVal: similar@ValueAnnotation, used to bind class properties with specific configuration, and to achieve configuration initialization and refresh when configuration changes

above@MetaValTwo points have been mentioned, one is initialization and the other is configuration refresh. Next, let’s see how to support these two points

a. Initialization

The precondition of initialization is to get all the members decorated with this annotation, and then use theMetaValHolderTo get the corresponding configuration and initialize

In order to achieve this, the best entry point is to get all the properties of the bean after the bean object is created to see whether it is marked with this annotationInstantiationAwareBeanPostProcessorAdapterTo achieve

b. Refresh

When the configuration changes, we also want the binding properties to change, so we need to save themto configureAndBean propertiesThe binding relationship between

Configuration changeAndRefresh bean propertiesThese two operations can be decoupled by spring’s event mechanism. When the configuration changes, aMetaChangeEventEvent, we provide an event handler by default, which is used to update the@MetaValBean properties of annotation binding

In addition to decoupling, another advantage of using events is that they are more flexible, such as supporting users’ extension of configuration usage

2. Implementation

1. Metaval notes

The binding relationship between configuration and bean properties is provided. Here we only provide a basic function of obtaining configuration according to configuration name. Interested partners can extend their support for spel

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MetaVal {

    /**
     *Get configured rules
     *
     * @return
     */
    String value() default "";

    /**
     *Meta value transforms the target object; currently provides basic data type support
     *
     * @return
     */
    MetaParser parser() default MetaParser.STRING_PARSER;
}

Please note that in addition to value, there is also a parser. Because our configuration value may be string or other basic types such as int and Boolean, we provide a basic type converter

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

We only define one interface here. The specific configuration acquisition is related to business requirements

public interface MetaValHolder {
    /**
     *Get configuration
     *
     * @param key
     * @return
     */
    String getProperty(String key);
}

In order 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. MetaValueRegisterConfigure binding and initialization

This class mainly provides scanning all beans and getting the@MetaValAnd initializes the

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 the binding relationship between configuration and field, and initialize
                    metaContainer.addInvokeCell(metaVal, bean, field);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
}

Please note that the core points above aremetaContainer.addInvokeCell(metaVal, bean, field);This business

4. MetaContainer

The configuration container saves the mapping relationship between configuration and field, and provides basic operation of configuration

@Slf4j
public class MetaContainer {
    private MetaValHolder metaValHolder;

    //Save the 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 relationship 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 update
    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

The next step is the support of event notification mechanism

Metachangeevent configures the change event. It provides three basic information: configuration key, original value and 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 the 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 above five steps, a custom configuration loader is basically completed, and the rest is the bean declaration

@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);
    }
}

It provides external use in the form of a two-party toolkit, so you need to create a new file in the resource directoryMETA-INF/spring.factories(routine)

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.git.hui.boot.dynamic.config.DynamicConfig

6. Testing

Complete the basic functions above, and then enter the test phase to customize a configuration loading

@Component
public class MetaPropertyHolder extends AbstractMetaValHolder {
    public Map<String, String> metas = new HashMap<>(8);

    {
        metas.put ("name", "a piece of ash");
        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);
    }
}

One useMetaValDemobean of

@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 follow [" + name + "] blog: + blog +" | "+ age;
    }

}

A simple rest service for viewing / updating configuration

@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);
    }
}

Dynamic diagram shows configuration acquisition and refresh process

[springboot basic series] implement a custom configuration loader (application)

When the configuration is refreshed, there will be log output, as shown below

[springboot basic series] implement a custom configuration loader (application)

2. Others

0. Project

Engineering source code

  • Project: https://github.com/liuyueyi/spring-boot-demo
  • Source code:- 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 blog

  • [DB series] ranking function realized by redis (application)
  • [DB series] build a simple site statistics service with redis (application)
  • [web series] implementation of back end interface version support (application)
  • [web series] a sample project of scanning code and login by hand (application part)
  • [basic series] AOP implements a log plug-in (application)
  • [basic series] implementation service mock for bean logoff and dynamic registration (application)
  • [basic series] implement a custom bean register from 0 to 1 (application)
  • [basic series] examples of SPI mechanism implemented by factorybean and proxy (application)
  • How to specify the bean to load first (application)
  • [basic series] implementation of a simple distributed timing task (application)

1. A gray blog

It’s not as good as a letter. The above contents are all from one family. Due to limited personal ability, there are inevitably omissions and mistakes. If you find a bug or have better suggestions, you are welcome to criticize and correct, and thank you

The following is a gray personal blog, recording all the blog articles in study and work. Welcome to visit

  • Personal blog https://blog.hhui.top
  • A grey blog spring blog http://spring.hhui.top

[springboot basic series] implement a custom configuration loader (application)