「SpringBoot 基礎系列」實現一個自定義配置加載器(應用篇)

2020-09-05 小灰灰blog

【SpringBoot 基礎系列】實現一個自定義配置加載器(應用篇)

Spring 中提供了@Value註解,用來綁定配置,可以實現從配置文件中,讀取對應的配置並賦值給成員變量;某些時候,我們的配置可能並不是在配置文件中,如存在 db/redis/其他文件/第三方配置服務,本文將手把手教你實現一個自定義的配置加載器,並支持@Value的使用姿勢

I. 環境 & 方案設計

1. 環境

  • SpringBoot 2.2.1.RELEASE
  • IDEA + JDK8

2. 方案設計

自定義的配置加載,有兩個核心的角色

  • 配置容器 MetaValHolder:與具體的配置打交道並提供配置
  • 配置綁定 @MetaVal:類似@Value註解,用於綁定類屬性與具體的配置,並實現配置初始化與配置變更時的刷新

上面@MetaVal提到了兩點,一個是初始化,一個是配置的刷新,接下來可以看一下如何支持這兩點

a. 初始化

初始化的前提是需要獲取到所有修飾有這個註解的成員,然後藉助MetaValHolder來獲取對應的配置,並初始化

為了實現上面這一點,最好的切入點是在 Bean 對象創建之後,獲取 bean 的所有屬性,查看是否標有這個註解,可以藉助InstantiationAwareBeanPostProcessorAdapter來實現

b. 刷新

當配置發生變更時,我們也希望綁定的屬性也會隨之改變,因此我們需要保存配置與bean屬性之間的綁定關係

配置變更 與 bean屬性的刷新 這兩個操作,我們可以藉助 Spring 的事件機制來解耦,當配置變更時,拋出一個MetaChangeEvent事件,我們默認提供一個事件處理器,用於更新通過@MetaVal註解綁定的 bean 屬性

使用事件除了解耦之外,另一個好處是更加靈活,如支持用戶對配置使用的擴展

II. 實現

1. MetaVal 註解

提供配置與 bean 屬性的綁定關係,我們這裡僅提供一個根據配置名獲取配置的基礎功能,有興趣的小夥伴可以自行擴展支持 SPEL

@Target({ElementType.FIELD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface MetaVal {    /**     * 獲取配置的規則     *     * @return     */    String value() default &34;;    /**     * meta value轉換目標對象;目前提供基本數據類型支持     *     * @return     */    MetaParser parser() default MetaParser.STRING_PARSER;}

請注意上面的實現,除了 value 之外,還有一個 parser,因為我們的配置 value 可能是 String,當然也可能是其他的基本類型如 int,boolean;所以提供了一個基本的類型轉換器

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

提供配置的核心類,我們這裡只定義了一個接口,具體的配置獲取與業務需求相關

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

為了支持配置刷新,我們提供一個基於 Spring 事件通知機制的抽象類

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 配置綁定與初始化

這個類,主要提供掃描所有的 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的綁定關係,並初始化                    metaContainer.addInvokeCell(metaVal, bean, field);                }            }        } catch (Exception e) {            e.printStackTrace();            System.exit(-1);        }    }}

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

4. MetaContainer

配置容器,保存配置與 field 映射關係,提供配置的基本操作

@Slf4jpublic class MetaContainer {    private MetaValHolder metaValHolder;    // 保存配置與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);    }    // 用於新增綁定關係並初始化    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(&34;, 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() + &34; + 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

接下來就是事件通知機制的支持了

MetaChangeEvent 配置變更事件,提供基本的三個信息,配置 key,原 value,新 value

@ToString@EqualsAndHashCodepublic 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 配置

上面五步,一個自定義的配置加載器基本上就完成了,剩下的就是 bean 的聲明

@Configurationpublic 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(常規套路了)

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

6. 測試

上面完成基本功能,接下來進入測試環節,自定義一個配置加載

@Componentpublic class MetaPropertyHolder extends AbstractMetaValHolder {    public Map<String, String> metas = new HashMap<>(8);    {        metas.put(&34;, &34;);        metas.put(&34;, &34;);        metas.put(&34;, &34;);    }    @Override    public String getProperty(String key) {        return metas.getOrDefault(key, &34;);    }    @Override    public String doUpdateProperty(String key, String value) {        return metas.put(key, value);    }}

一個使用MetaVal的 demoBean

@Componentpublic class DemoBean {    @MetaVal(&34;)    private String name;    @MetaVal(&34;)    private String blog;    @MetaVal(value = &34;, parser = MetaParser.INT_PARSER)    private Integer age;    public String sayHello() {        return &34; + name + &34; + blog + &34; + age;    }}

一個簡單的 REST 服務,用於查看/更新配置

@RestControllerpublic class DemoAction {    @Autowired    private DemoBean demoBean;    @Autowired    private MetaPropertyHolder metaPropertyHolder;    @GetMapping(path = &34;)    public String hello() {        return demoBean.sayHello();    }    @GetMapping(path = &34;)    public String updateBlog(@RequestParam(name = &34;) String key, @RequestParam(name = &34;) String val,            HttpServletResponse response) throws IOException {        metaPropertyHolder.updateProperty(key, val);        response.sendRedirect(&34;);        return &34;;    }}

啟動類

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

動圖演示配置獲取和刷新過程

配置刷新時,會有日誌輸出,如下

II. 其他

0. 項目

工程源碼

  • 工程:https://github.com/liuyueyi/spring-boot-demo
  • 源碼: - 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

推薦博文

  • 【DB 系列】藉助 Redis 實現排行榜功能(應用篇)
  • 【DB 系列】藉助 Redis 搭建一個簡單站點統計服務(應用篇)
  • 【WEB 系列】實現後端的接口版本支持(應用篇)
  • 【WEB 系列】徒手擼一個掃碼登錄示例工程(應用篇)
  • 【基礎系列】AOP 實現一個日誌插件(應用篇)
  • 【基礎系列】Bean 之註銷與動態註冊實現服務 mock(應用篇)
  • 【基礎系列】從0到1實現一個自定義Bean註冊器(應用篇)
  • 【基礎系列】FactoryBean及代理實現SPI機制的實例(應用篇)
  • 【基礎系列-實戰】如何指定bean最先加載(應用篇)
  • 【基礎系列】實現一個簡單的分布式定時任務(應用篇)

一灰灰blog

相關焦點

  • SpringBoot圖文教程「概念+案例 思維導圖」「基礎篇上」
    我向來是不憚以最多的無聊揣測這個假期的,但我沒想到……(不能再往下了,再往下真就算抄襲了)於是決定將自己的的畢生功力匯聚整理成冊,寫出《圖文教程》系列Java技術學習秘籍,本功法力求 『圖文並茂』 『簡單易懂』,概念和代碼實踐相結合,每個知識點輔助以自測面試題,希望大家通過本系列教程能夠快樂學Java,從練氣到飛升。
  • 如何使用自定義類加載器防止代碼被反編譯破解
    今天我們就來聊聊如何通過對代碼進行加密實現代碼防反編譯,至於混淆因為可以直接利用proguard-maven-plugin進行配置實現,相對比較簡單,就不在本文論述代碼防編譯整體套路我們正常classpath路徑下的類都是通過系統類加載器進行加載。而不巧這三個jdk提供的加載器沒法滿足我們的需求。因此我們只能自己實現我們的類加載器。
  • 兩小時入門SpringBoot學習(基礎)(上)
    第一步,使用spring Initializr新建一個項目ispringboot,具體操作如下(按照圖中序號依次進行即可):第三種:先在項目根路徑(也就是pom.xml所在目錄)下執行maven命令mvn install,接著進入到target目錄,然後在target目錄下執行java -jar luckymoney-0.0.1-SNAPSHOT.jar即可(注意這種方式是springboot項目脫機後運行的方式)
  • 「JVM篇」類加載器的三種分類及雙親委派模式原理詳解
    )擴展類加載器(ExtClassLoader)應用程式類加載器(AppClassLoader)1、啟動類加載器啟動類加載器是由c++實現的,是虛擬機的一部分,主要負責加載jvm自身需要的類,即負責加載$AVAHOME$下的核心類庫。
  • JVM的藝術—類加載器篇
    自定義類加載器需要加載類時,先委託應用類加載器去加載,然後應用類加載器又向擴展類加載器去委託,擴展類加載器在向啟動類加載器去委託。如果啟動類加載器不能加載該類。Person的時候,根據雙親委託模型,我們的Person並沒有被自定義類加載(Test01ClassLoader)加載,而是被AppClassloader加載成功,同時根據全盤委託規則,我們的Dog類也被AppClassLoader加載了。
  • Java 中的「類加載機制」是怎樣的呢?
    是用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader;ExtClassLoader 加載 JVM 提供的擴展庫目錄下的 Java 類;AppClassLoader 是根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。
  • 企業級SpringBoot應用多個子項目配置文件規劃、多環境支持(二)
    採用springboot項目,默認是把配置文件和lib依賴都打到了一個jar中,我們在運行的時候直接採用java -jar xxx.jar這種方式簡化了jar包的啟動,但是也有不好的地方。>總結介紹到這裡我們已經做到了配置文件外部化了,大型項目在配置規劃時,會引入配置中心這個概念(這個之前老顧已經介紹過了),不需要引入子項目config,直接連接配置中心即可。
  • 微服務實戰系列(八)-網關gateway自定義規則
    場景描述先說明下項目中使用的網關是:springcloud gateway, 因需要給各個網關服務系統提供自定義配置路由規則,實時生效,不用重啟網關(重啟風險大),目前已實現:動態加載自定義路由文件,動態加載路由文件中的路由規則。
  • SpringBoot&FreeMarker之零配置自定義指令(附Git源碼)
    Apache FreeMarker是一個基於Java庫的視圖模板引擎,主要用於根據模板和模型數據渲染生成文本輸出(HTML網頁,電子郵件,配置文件,原始碼等等)。FreeMarker模板使用其特定的模板語言(FTL)進行編寫,這是一種簡單的專用標記語言。通常,使用通用程式語言(如Java)來準備模型數據;然後,Apache FreeMarker使用模板渲染準備好的數據。
  • 09-springboot工程中的熱部署實現
    熱部署簡介Spring Boot 開發者為Spring Boot項目中提供了一個熱部署(spring-boot-devtools)模塊,支持項目的熱部署(修改了某些資源以後無需重啟服務),以提高開發效率.其底層其實是藉助了兩個類加載器做了具體實現,一個類加載器加載不變class,一個類加載器加載可能變化類,以提供類的熱部署性能
  • 從零手寫並發框架(四)異步轉同步 springboot 整合
    )異步查詢轉同步的 7 種實現方式java 手寫並發框架(二)異步轉同步框架封裝鎖策略java 手寫並發框架(三)異步轉同步框架註解和字節碼增強java 手寫並發框架(四)異步轉同步框架spring整合整合思路
  • springboot自動配置原理看這篇文章就夠了
    前言隨著網際網路越來越流行,springboot已經成為我們無論是工作,還是面試當中,不得不掌握的技術。說起springboot筆者認為最重要的功能非自動配置莫屬了,為什麼這麼說?如果參與過以前spring複雜項目的朋友肯定,有過這樣的經歷,每次需要一個新功能,比如事務、AOP等,需要大量的配置,需要導出找jar包,時不時會出現jar兼容性問題,可以說苦不堪言。springboot的出現得益於「習慣優於配置」的理念,沒有繁瑣的配置、難以集成的內容(大多數流行第三方技術都被集成),這是基於Spring 4.x以上的版本提供的按條件配置Bean的能力。
  • Nginx+SpringBoot實現負載均衡
    負載均衡介紹介紹在介紹Nginx的負載均衡實現之前,先簡單的說下負載均衡的分類,主要分為「硬體負載均衡和軟體負載均衡」,硬體負載均衡是使用專門的軟體和硬體相結合的設備least_time=last_byte (NGINX Plus) :從伺服器接收完整響應的最短平均時間($upstream_response_time)。
  • 「SpringBoot WEB 系列」RestTemplate 之自定義請求頭
    【WEB 系列】RestTemplate 之自定義請求頭上一篇介紹了 RestTemplate 的基本使用姿勢,在文末提出了一些擴展的高級使用姿勢,本篇將主要集中在如何攜帶自定義的請求頭,如設置 User-Agent,攜帶 CookieGet 攜帶請求頭Post 攜帶請求頭攔截器方式設置統一請求頭
  • 線程上下文類加載器ContextClassLoader內存洩漏隱患
    JVM不提供類卸載的功能,從目前參考到的資料來看,類卸載需要滿足下面幾點:條件一:Class的所有實例不被強引用(不可達)。條件二:Class本身不被強引用(不可達)。條件三:加載該Class的ClassLoader實例不被強引用(不可達)。有些場景下需要實現類的熱部署和卸載,例如定義一個接口,然後由外部動態傳入代碼的實現。
  • SpringBoot系列教程web篇之如何自定義參數解析器
    SpringMVC提供了各種姿勢的http參數解析支持,從前面的GET/POST參數解析篇也可以看到,加一個 @RequsetParam註解就可以將方法參數與http參數綁定,看到這時自然就會好奇這是怎麼做到的,我們能不能自己定義一種參數解析規則呢?本文將介紹如何實現自定義的參數解析,並讓其生效I.
  • SpringBoot AOP 實現埋點日誌記錄(完整源碼)
    本文主要介紹使用springboot aop 自定義註解方式實現埋點日誌記錄。1、配置文件這裡只有三個配置:server.port=8081,設置項目啟動的埠號,防止被其他服務佔用server.servlet.context-path: /aop,項目上下文spring.aop.auto=true,開啟spring的aop配置,簡單明了,不需要多配置其他的配置或註解
  • 兩小時入門SpringBoot學習(基礎)(下)
    接下來說一下請求方式,這裡面的say方法使用的是@GetMapping(&34;),(@GetMapping是@RequestMapping(method = RequestMethod.GET)的縮寫。)
  • 「機器學習」機器學習算法優缺點對比(匯總篇)
    然而,隨著你訓練集的增長,模型對於原數據的預測能力就越好,偏差就會降低,此時低偏差/高方差的分類器就會漸漸的表現其優勢(因為它們有較低的漸近誤差),而高偏差分類器這時已經不足以提供準確的模型了。「為什麼說樸素貝葉斯是高偏差低方差?」以下內容引自知乎:首先,假設你知道訓練集和測試集的關係。
  • springboot自定義註解,怎麼搞?
    Java枚舉通常springboot的應用場景為:日誌記錄: 記錄請求信息的日誌, 以便進行信息監控, 信息統計, 計算PV(page View)等性能監控;權限檢查;通用行為自定義註解的範式@Target 註解主要說明註解的使用範圍,主要包括以下幾種類型:TYPE:類,接口或者枚舉FIELD:域,包含枚舉常量METHOD:方法PARAMETER:參數CONSTRUCTOR:構造方法LOCAL_VARIABLE:局部變量ANNOTATION_TYPE:註解類型PACKAGE:包