在Spring中實(shí)現(xiàn)類似SpringBoot的環(huán)境檢測(cè)能力

在Spring中實(shí)現(xiàn)類似SpringBoot的環(huán)境檢測(cè)能力

前言

? 在Boot 你的應(yīng)用一文中提到了有時(shí)候我們需要檢測(cè)當(dāng)前時(shí)環(huán)境是否匹配我們的運(yùn)行時(shí)要求,并根據(jù)不同的環(huán)境進(jìn)行個(gè)性化的適配粒竖。

? Spring4已經(jīng)引入了簡(jiǎn)單的擴(kuò)展接口 @ConditionalCondition鸳惯,允許大家自行去識(shí)別環(huán)境信息汰规,但也僅此而已波附,并沒(méi)有內(nèi)置一些可以讓大家在實(shí)際場(chǎng)景中使用的條件判定器杂曲。

? 真正將 @ConditionalCondition發(fā)揚(yáng)光大的是SpringBoot诈火,在SpringBoot是全面采用了 AutoConfiguration@Conditional將自動(dòng)配置的強(qiáng)大功能展現(xiàn)得淋漓盡致兽赁,內(nèi)置了超過(guò)10種不同類型支持超過(guò)100種不同場(chǎng)景的環(huán)境檢測(cè)器。比如:檢測(cè)當(dāng)前環(huán)境中是否存在某個(gè)Class,檢測(cè)當(dāng)前容器中是否定義了某個(gè)SpringBean刀崖,檢測(cè)當(dāng)前是否有某個(gè)配置項(xiàng)惊科,配置項(xiàng)的值是多少等等。所有的環(huán)境檢測(cè)器都在 org.springframework.boot.autoconfigure.condition 下面亮钦,大家可以去翻閱源碼學(xué)習(xí)了解馆截。

@Conditional 與 Condition 介紹

? 前文提到在 Spring 框架中僅僅提供了這兩個(gè)擴(kuò)展點(diǎn),并沒(méi)有能運(yùn)用在實(shí)際應(yīng)用場(chǎng)景中的環(huán)境檢測(cè)器蜂莉,這一節(jié)我們將分析這兩個(gè)接口蜡娶,并實(shí)現(xiàn)一個(gè)簡(jiǎn)單的環(huán)境檢測(cè)功能。

? 以下是@Conditional的源碼:

/**
 * Indicates that a component is only eligible for registration when all
 * {@linkplain #value() specified conditions} match.
 *
 * <p>A <em>condition</em> is any state that can be determined programmatically
 * before the bean definition is due to be registered (see {@link Condition} for details).
 *
 * <p>The {@code @Conditional} annotation may be used in any of the following ways:
 * <ul>
 * <li>as a type-level annotation on any class directly or indirectly annotated with
 * {@code @Component}, including {@link Configuration @Configuration} classes</li>
 * <li>as a meta-annotation, for the purpose of composing custom stereotype
 * annotations</li>
 * <li>as a method-level annotation on any {@link Bean @Bean} method</li>
 * </ul>
 *
 * <p>If a {@code @Configuration} class is marked with {@code @Conditional}, all of the
 * {@code @Bean} methods, {@link Import @Import} and {@link ComponentScan @ComponentScan}
 * annotations associated with that class will be subject to the conditions.
 *
 * <p>NOTE: {@code @Conditional} annotations are not inherited; any conditions from
 * superclasses or from overridden methods are not being considered.
 *
 * @author Phillip Webb
 * @since 4.0
 * @see Condition
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Conditional {

    /**
     * All {@link Condition}s that must {@linkplain Condition#matches match}
     * in order for the component to be registered.
     */
    Class<? extends Condition>[] value();

}

? 這是一個(gè)注解巡语,從注釋中我們看到這是 @Since 4.0 的翎蹈,即在 Spring4 開(kāi)始提供的,用來(lái)指定一系列的配置條件男公,當(dāng)所有指定的條件都滿足時(shí)荤堪,被 @Configuration 中標(biāo)注的 @Bean@Import枢赔,@ComponentScan才會(huì)生效澄阳。

? 它接受一個(gè)Condition數(shù)組,用來(lái)標(biāo)記所有的篩選條件踏拜,當(dāng)所有的Condition.matches條件均返回true時(shí)即可認(rèn)為該Conditional成立碎赢,從而完成環(huán)境檢測(cè)。

? Condition接口只有一個(gè)方法速梗,源碼如下:

/**
 * A single {@code condition} that must be {@linkplain #matches matched} in order
 * for a component to be registered.
 *
 * <p>Conditions are checked immediately before the bean-definition is due to be
 * registered and are free to veto registration based on any criteria that can
 * be determined at that point.
 *
 * <p>Conditions must follow the same restrictions as {@link BeanFactoryPostProcessor}
 * and take care to never interact with bean instances. For more fine-grained control
 * of conditions that interact with {@code @Configuration} beans consider the
 * {@link ConfigurationCondition} interface.
 *
 * @author Phillip Webb
 * @since 4.0
 * @see ConfigurationCondition
 * @see Conditional
 * @see ConditionContext
 */
public interface Condition {

    /**
     * Determine if the condition matches.
     * @param context the condition context
     * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
     * or {@link org.springframework.core.type.MethodMetadata method} being checked.
     * @return {@code true} if the condition matches and the component can be registered
     * or {@code false} to veto registration.
     */
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

? 只需要實(shí)現(xiàn)matches方法并根據(jù)自己的需要完成環(huán)境檢測(cè)判定即可肮塞。

?

簡(jiǎn)單用法示例

? 下面我們用一個(gè)小的示例來(lái)演示這兩個(gè)接口的使用方法,假設(shè)需求:根據(jù)不同的操作系統(tǒng)注冊(cè)不同的 MXBean 服務(wù)

1. 實(shí)現(xiàn)在不同操作系統(tǒng)環(huán)境下的條件判定

? 這個(gè)過(guò)程我們就簡(jiǎn)化地判定當(dāng)前的os.name就可以姻锁,代碼如下:

Windows 環(huán)境的判定器

/**
 * 判定當(dāng)前環(huán)境是否為 Windows 的條件
 *
 * @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
 * created on 2018/12/10.
 */
public class WindowsCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").contains("Windows");
    }
}

Linux 環(huán)境的判定器

/**
 * 判定當(dāng)前環(huán)境是否為 Windows 的條件
 *
 * @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
 * created on 2018/12/10.
 */
public class WindowsCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("os.name").contains("Linux");
    }
}

2. 在Bean注冊(cè)時(shí)帶上條件注解

? 有了第1步的的條件判定器枕赵,那么在我們進(jìn)行 @Configuraiton的Bean注冊(cè)時(shí)就可以將這些條件附帶上,讓Spring容器根據(jù)不同的條件加載不同的Bean配置位隶。代碼如下所示:

/**
 * 根據(jù)不同的操作系統(tǒng)加載不同的 MXBean
 *
 * @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
 * created on 2018/12/10.
 */
@Configuration
public class ConditionalMXBeanConifg {
    
    @Bean
    @Conditional(WindowsCondition.class)
    public BaseMXBean windowsMXBeanService() {
        return new WindowsMXBean();
    }

    @Bean
    @Conditional(LinuxCondition.class)
    public BaseMXBean linuxMXBeanService() {
        return new LinuxMXBean();
    }
}

根據(jù)以上的配置拷窜,在不同的操作系統(tǒng)環(huán)境下,Spring會(huì)分別注冊(cè)不同的 MXBean 涧黄。

高級(jí)用法示例

? 在簡(jiǎn)單用法示例中我們可以看到篮昧,雖然實(shí)現(xiàn)了不同環(huán)境下的判定識(shí)別,但還是太簡(jiǎn)單了笋妥,還是比較靜態(tài)的懊昨,如果我們要像SpringBoot那樣動(dòng)態(tài)的判定當(dāng)前環(huán)境中是否存在某個(gè)類,Spring容器中是否存在某個(gè)Bean定義該怎么做呢春宣?下面我們將演示這幾種更高級(jí)的用法疚颊。

判定當(dāng)前 classpath 下是否存在某個(gè)類

? 這類條件判定器主要用在一些模板類SDK中狈孔,根據(jù)當(dāng)前用戶是否依賴了某些類來(lái)確定是否要定義相應(yīng)的模板、工具材义、服務(wù)等均抽。如同應(yīng)用分發(fā) base-boot-starter 的使用說(shuō)明中對(duì)于 OA 權(quán)限平臺(tái)的判定一樣,當(dāng)用戶沒(méi)有添加OA權(quán)限平臺(tái)這個(gè)MAVEN依賴時(shí)其掂,應(yīng)用仍然能智能判定而不是拋出 NoClassDefFoundError油挥。

  1. 首先定義一個(gè)自定義的注解,供用戶使用判定

    /**
     * 是否存在某個(gè)類的條件判定注解
     *
     * @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
     * Time: 2018/12/7 : 19:34
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(OnClassCondition.class)
    public @interface ConditionalOnClass {
    
        /**
         * 必須存在的類
         *
         * @return 必須存在的類
         */
        Class<?>[] value() default {};
    
        /**
         * 必須存在的類名
         *
         * @return 必須存在的類名
         */
        String[] name() default {};
    
    }
    

當(dāng)用戶在注解某個(gè)@Bean進(jìn)款熬,可以添加這個(gè)注解來(lái)進(jìn)行判定深寥。這個(gè)注解本身還依賴另外一個(gè)注解@Conditional(OnClassCondition.class),表示掃Spring容器在掃描到某個(gè)類定義被標(biāo)注了@ConditionalOnClass時(shí)贤牛,會(huì)執(zhí)行里面的OnClassCondition來(lái)完成條件判定惋鹅。

  1. 定義真正的條件判定器 OnClassCondition

    /**
     * 判定某個(gè)類是否存在的條件
     *
     * @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
     * Time: 2018/12/7 : 19:29
     */
    public class OnClassCondition extends BaseCondition implements Condition {
    
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    
            MultiValueMap<String, Object> attributes = metadata
                    .getAllAnnotationAttributes(ConditionalOnClass.class.getName(), true);
            if (null == attributes) {
                return false;
            }
            List<String> candidates = new ArrayList<>();
            addAll(candidates, attributes.get("value"));
            addAll(candidates, attributes.get("name"));
    
            for (String candidate : candidates) {
                if (!ClassUtils.isPresent(candidate, null)) {
                    return false;
                }
            }
    
            return true;
        }
    }
    

    其實(shí)也很簡(jiǎn)單,就是從注解中獲取當(dāng)前用戶要判定是否存在的Class(支持類定義殉簸,和全類名)闰集,然后在當(dāng)前 classpath 下去查找這個(gè)類是否存在即完成判定過(guò)程。

  2. 使用自定義的注解

    使用起來(lái)就比較簡(jiǎn)單了般卑,加上注解即可武鲁,如下所示:

    @ConditionalOnClass(SSOFilter.class)
    @Configuration
    public class SsoAutoConfiguration {...}
    

判定當(dāng)前Spring容器中是否定義了某個(gè)Bean

? 這類判定主要用在如下的場(chǎng)景:某些組件需要依賴某個(gè)SpringBean,如果當(dāng)前Spring容器中不存在這個(gè)Bean蝠检,那么就要添加一個(gè)沐鼠,如果存在就不能再添加,防止產(chǎn)生NoSuchBeanDefinitionException或者NoUniqueBeanDefinitionException異常叹谁。

? 其實(shí)現(xiàn)過(guò)程其實(shí)和@ConditionalOnClass大同小異饲梭,最主要的區(qū)別在于Condition.matches方法一個(gè)是判定類是否存在,一個(gè)是判定Bean是否存在焰檩。代碼如下:

/**
 * 判定某個(gè) bean 是否存在的條件
 *
 * @author <a href="mailto:huangfengjing@gmail.com">Ivan</a>
 * Time: 2018/12/7 : 19:29
 */
@Slf4j
public class OnBeanCondition extends BaseCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

        MultiValueMap<String, Object> conditionOnBeanAttrs = metadata
                .getAllAnnotationAttributes(ConditionalOnBean.class.getName(), true);
        if (null != conditionOnBeanAttrs) {
            return matchBean(context, conditionOnBeanAttrs, metadata);
        }

        MultiValueMap<String, Object> conditionOnMissingBeanAttrs = metadata
                .getAllAnnotationAttributes(ConditionalOnMissingBean.class.getName(), true);
        if (conditionOnMissingBeanAttrs != null) {
            return matchMissingBean(context, conditionOnMissingBeanAttrs, metadata);
        }
        return false;
    }

    private boolean matchBean(ConditionContext context, MultiValueMap<String, Object> attributes, AnnotatedTypeMetadata metadata) {
        if (attributes == null) {
            return false;
        }

        BeanFactory beanFactory = context.getBeanFactory();

        List<String> classNameCandidates = new ArrayList<>();
        addAll(classNameCandidates, attributes.get("value"));
        try {
            for (String clsName : classNameCandidates) {
                beanFactory.getBean(Class.forName(clsName));
            }
        } catch (Exception e) {
            log.debug("沒(méi)有找到需要的 Bean: {}", e.getMessage());
            return false;
        }

        List<String> beanNameCandidates = new ArrayList<>();
        addAll(beanNameCandidates, attributes.get("name"));
        for (String beanName : beanNameCandidates) {
            if (!beanFactory.containsBean(beanName)) {
                log.debug("沒(méi)有找到需要的 bean: {}", beanName);
                return false;
            }
        }

        return true;
    }

    private boolean matchMissingBean(ConditionContext context, MultiValueMap<String, Object> attributes, AnnotatedTypeMetadata metadata) {
        // ... 和 matchBean 相反憔涉,判定是否不存在某個(gè) Bean,省略
    }
}

總結(jié)

? 本文主要講解了如何通過(guò)@ConditionalCondition實(shí)現(xiàn)環(huán)境檢測(cè)的能力锅尘,并從源碼及示例兩方面演示了從簡(jiǎn)單到高級(jí)的用法支持监氢。其它的諸如判定當(dāng)時(shí)配置項(xiàng)中的值以及資源判定的實(shí)現(xiàn)原理都差不多布蔗,感興趣的可以翻閱應(yīng)用分發(fā) base-boot-starter 的源碼藤违。

當(dāng)然,這里高級(jí)應(yīng)用里面的判定規(guī)則并不如SpringBoot中的功能強(qiáng)大纵揍,但應(yīng)付常規(guī)的應(yīng)用已經(jīng)足夠顿乒,當(dāng)不滿足需求時(shí),通過(guò)本文的講解讀者應(yīng)該也已經(jīng)了解到了如何自行擴(kuò)展泽谨,或者聯(lián)系我協(xié)助璧榄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末特漩,一起剝皮案震驚了整個(gè)濱河市孤页,隨后出現(xiàn)的幾起案子伶贰,更是在濱河造成了極大的恐慌,老刑警劉巖儡蔓,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搓蚪,死亡現(xiàn)場(chǎng)離奇詭異蛤售,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)妒潭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門(mén)悴能,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人雳灾,你說(shuō)我怎么就攤上這事漠酿。” “怎么了谎亩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵炒嘲,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我团驱,道長(zhǎng)摸吠,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任嚎花,我火速辦了婚禮寸痢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘紊选。我一直安慰自己啼止,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布兵罢。 她就那樣靜靜地躺著献烦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卖词。 梳的紋絲不亂的頭發(fā)上巩那,一...
    開(kāi)封第一講書(shū)人閱讀 52,874評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音此蜈,去河邊找鬼即横。 笑死,一個(gè)胖子當(dāng)著我的面吹牛裆赵,可吹牛的內(nèi)容都是我干的东囚。 我是一名探鬼主播,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼战授,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼页藻!你這毒婦竟也來(lái)了桨嫁?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤份帐,失蹤者是張志新(化名)和其女友劉穎璃吧,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體废境,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肚逸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了彬坏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朦促。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖栓始,靈堂內(nèi)的尸體忽然破棺而出务冕,到底是詐尸還是另有隱情,我是刑警寧澤幻赚,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布禀忆,位于F島的核電站,受9級(jí)特大地震影響落恼,放射性物質(zhì)發(fā)生泄漏箩退。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一佳谦、第九天 我趴在偏房一處隱蔽的房頂上張望戴涝。 院中可真熱鬧,春花似錦钻蔑、人聲如沸啥刻。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)可帽。三九已至,卻和暖如春窗怒,著一層夾襖步出監(jiān)牢的瞬間映跟,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來(lái)泰國(guó)打工扬虚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留努隙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓孔轴,卻偏偏與公主長(zhǎng)得像剃法,于是被迫代替她去往敵國(guó)和親碎捺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子路鹰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 本來(lái)是準(zhǔn)備看一看Spring源碼的贷洲。然后在知乎上看到來(lái)一個(gè)帖子,說(shuō)有一群**自己連Spring官方文檔都沒(méi)有完全讀...
    此魚(yú)不得水閱讀 6,939評(píng)論 4 21
  • 第三章 高級(jí)裝配 標(biāo)簽(空格分隔): 未分類 [TOC] 環(huán)境與profile 配置profile bean 在開(kāi)...
    施瓦閱讀 390評(píng)論 0 0
  • spring官方文檔:http://docs.spring.io/spring/docs/current/spri...
    牛馬風(fēng)情閱讀 1,691評(píng)論 0 3
  • 為什么到簡(jiǎn)書(shū)來(lái) 朋友圈晋柱,微博优构,空間…… 在越來(lái)越多的網(wǎng)絡(luò)社交里變得越來(lái)越浮躁 缺少一個(gè)安安靜靜的地方認(rèn)真生活 第一...
    某木a閱讀 279評(píng)論 0 1
  • 夜晚從窗往外望,是一扇一扇的人生雁竞,大家那么不一樣钦椭,甚至是家里光的顏色和亮度都是不一樣的。所以人如果像遇到跟自己完全...
    二狗站住閱讀 230評(píng)論 0 0