【SpringBoot 基礎(chǔ)系列】實(shí)現(xiàn)一個(gè)自定義配置加載器(應(yīng)用篇)

image

【SpringBoot 基礎(chǔ)系列】實(shí)現(xiàn)一個(gè)自定義配置加載器(應(yīng)用篇)

Spring 中提供了@Value注解,用來綁定配置,可以實(shí)現(xiàn)從配置文件中耕拷,讀取對應(yīng)的配置并賦值給成員變量种樱;某些時(shí)候,我們的配置可能并不是在配置文件中腕巡,如存在 db/redis/其他文件/第三方配置服務(wù)玄坦,本文將手把手教你實(shí)現(xiàn)一個(gè)自定義的配置加載器,并支持@Value的使用姿勢

I. 環(huán)境 & 方案設(shè)計(jì)

1. 環(huán)境

  • SpringBoot 2.2.1.RELEASE
  • IDEA + JDK8

2. 方案設(shè)計(jì)

自定義的配置加載绘沉,有兩個(gè)核心的角色

  • 配置容器 MetaValHolder:與具體的配置打交道并提供配置
  • 配置綁定 @MetaVal:類似@Value注解煎楣,用于綁定類屬性與具體的配置,并實(shí)現(xiàn)配置初始化與配置變更時(shí)的刷新

上面@MetaVal提到了兩點(diǎn)车伞,一個(gè)是初始化择懂,一個(gè)是配置的刷新,接下來可以看一下如何支持這兩點(diǎn)

a. 初始化

初始化的前提是需要獲取到所有修飾有這個(gè)注解的成員另玖,然后借助MetaValHolder來獲取對應(yīng)的配置困曙,并初始化

為了實(shí)現(xiàn)上面這一點(diǎn)表伦,最好的切入點(diǎn)是在 Bean 對象創(chuàng)建之后,獲取 bean 的所有屬性慷丽,查看是否標(biāo)有這個(gè)注解蹦哼,可以借助InstantiationAwareBeanPostProcessorAdapter來實(shí)現(xiàn)

b. 刷新

當(dāng)配置發(fā)生變更時(shí),我們也希望綁定的屬性也會(huì)隨之改變要糊,因此我們需要保存配置bean屬性之間的綁定關(guān)系

配置變更bean屬性的刷新 這兩個(gè)操作纲熏,我們可以借助 Spring 的事件機(jī)制來解耦,當(dāng)配置變更時(shí)杨耙,拋出一個(gè)MetaChangeEvent事件赤套,我們默認(rèn)提供一個(gè)事件處理器,用于更新通過@MetaVal注解綁定的 bean 屬性

使用事件除了解耦之外珊膜,另一個(gè)好處是更加靈活容握,如支持用戶對配置使用的擴(kuò)展

II. 實(shí)現(xiàn)

1. MetaVal 注解

提供配置與 bean 屬性的綁定關(guān)系,我們這里僅提供一個(gè)根據(jù)配置名獲取配置的基礎(chǔ)功能车柠,有興趣的小伙伴可以自行擴(kuò)展支持 SPEL

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

    /**
     * 獲取配置的規(guī)則
     *
     * @return
     */
    String value() default "";

    /**
     * meta value轉(zhuǎn)換目標(biāo)對象剔氏;目前提供基本數(shù)據(jù)類型支持
     *
     * @return
     */
    MetaParser parser() default MetaParser.STRING_PARSER;
}

請注意上面的實(shí)現(xiàn),除了 value 之外竹祷,還有一個(gè) parser谈跛,因?yàn)槲覀兊呐渲?value 可能是 String,當(dāng)然也可能是其他的基本類型如 int塑陵,boolean感憾;所以提供了一個(gè)基本的類型轉(zhuǎn)換器

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

提供配置的核心類,我們這里只定義了一個(gè)接口令花,具體的配置獲取與業(yè)務(wù)需求相關(guān)

public interface MetaValHolder {
    /**
     * 獲取配置
     *
     * @param key
     * @return
     */
    String getProperty(String key);
}

為了支持配置刷新阻桅,我們提供一個(gè)基于 Spring 事件通知機(jī)制的抽象類

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

    /**
     * 更新配置
     *
     * @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 配置綁定與初始化

這個(gè)類,主要提供掃描所有的 bean兼都,并獲取到@MetaVal修飾的屬性嫂沉,并初始化

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

    /**
     * 掃描bean的所有屬性,并獲取@MetaVal修飾的屬性
     * @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) {
                    // 緩存配置與Field的綁定關(guān)系扮碧,并初始化
                    metaContainer.addInvokeCell(metaVal, bean, field);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }
}

請注意趟章,上面核心點(diǎn)在metaContainer.addInvokeCell(metaVal, bean, field);這一行

4. MetaContainer

配置容器,保存配置與 field 映射關(guān)系慎王,提供配置的基本操作

@Slf4j
public class MetaContainer {
    private MetaValHolder metaValHolder;

    // 保存配置與Field之間的綁定關(guān)系
    private Map<String, Set<InvokeCell>> metaCache = new ConcurrentHashMap<>();

    public MetaContainer(MetaValHolder metaValHolder) {
        this.metaValHolder = metaValHolder;
    }

    public String getProperty(String key) {
        return metaValHolder.getProperty(key);
    }

    // 用于新增綁定關(guān)系并初始化
    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)));
    }

    // 配置更新
    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

接下來就是事件通知機(jī)制的支持了

MetaChangeEvent 配置變更事件蚓土,提供基本的三個(gè)信息,配置 key赖淤,原 value北戏,新 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 事件處理器,刷新@MetaVal 綁定的配置

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 配置

上面五步漫蛔,一個(gè)自定義的配置加載器基本上就完成了嗜愈,剩下的就是 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);
    }
}

以二方工具包方式提供外部使用,所以需要在資源目錄下莽龟,新建文件META-INF/spring.factories(常規(guī)套路了)

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

6. 測試

上面完成基本功能蠕嫁,接下來進(jìn)入測試環(huán)節(jié),自定義一個(gè)配置加載

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

    {
        metas.put("name", "一灰灰");
        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);
    }
}

一個(gè)使用MetaVal的 demoBean

@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 "歡迎關(guān)注 [" + name + "] 博客:" + blog + " | " + age;
    }

}

一個(gè)簡單的 REST 服務(wù)毯盈,用于查看/更新配置

@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!";
    }
}

啟動(dòng)類

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }
}

動(dòng)圖演示配置獲取和刷新過程

image

配置刷新時(shí)剃毒,會(huì)有日志輸出,如下

image

II. 其他

0. 項(xiàng)目

工程源碼

推薦博文

1. 一灰灰 Blog

盡信書則不如搂赋,以上內(nèi)容赘阀,純屬一家之言,因個(gè)人能力有限脑奠,難免有疏漏和錯(cuò)誤之處基公,如發(fā)現(xiàn) bug 或者有更好的建議,歡迎批評(píng)指正宋欺,不吝感激

下面一灰灰的個(gè)人博客轰豆,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛

一灰灰blog
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末齿诞,一起剝皮案震驚了整個(gè)濱河市酸休,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌祷杈,老刑警劉巖斑司,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異但汞,居然都是意外死亡宿刮,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門特占,熙熙樓的掌柜王于貴愁眉苦臉地迎上來糙置,“玉大人,你說我怎么就攤上這事是目“梗” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵懊纳,是天一觀的道長揉抵。 經(jīng)常有香客問我,道長嗤疯,這世上最難降的妖魔是什么冤今? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮茂缚,結(jié)果婚禮上戏罢,老公的妹妹穿的比我還像新娘屋谭。我一直安慰自己,他們只是感情好龟糕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布桐磁。 她就那樣靜靜地躺著,像睡著了一般讲岁。 火紅的嫁衣襯著肌膚如雪我擂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天缓艳,我揣著相機(jī)與錄音校摩,去河邊找鬼。 笑死阶淘,一個(gè)胖子當(dāng)著我的面吹牛衙吩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舶治,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼分井,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霉猛?” 一聲冷哼從身側(cè)響起尺锚,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惜浅,沒想到半個(gè)月后瘫辩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坛悉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年伐厌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裸影。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挣轨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轩猩,到底是詐尸還是另有隱情卷扮,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布均践,位于F島的核電站晤锹,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏彤委。R本人自食惡果不足惜鞭铆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望焦影。 院中可真熱鬧车遂,春花似錦封断、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至柄沮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間废岂,已是汗流浹背祖搓。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留湖苞,地道東北人拯欧。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像财骨,于是被迫代替她去往敵國和親镐作。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355