天天看點

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

Spring Boot 是由 Pivotal 團隊提供的全新架構,其設計目的是用來簡化新 Spring 應用的初始搭建以及開發過程。該架構通過約定由于配置的原則,來進行簡化配置。Spring Boot緻力于在蓬勃發展的快速應用開發領域成為上司者。Spring Boot 目前廣泛應用與各大網際網路公司,有以下特點:

  1. 建立獨立的 Spring 應用程式
  2. 嵌入的 Tomcat,無需部署 WAR 檔案
  3. 簡化 Maven 配置
  4. 自動配置 Spring
  5. 提供生産就緒型功能,如名額,健康檢查和外部配置
  6. 絕對沒有代碼生成,對 XML 沒有要求配置

并且 Spring Boot 可以與Spring Cloud、Docker完美內建,是以我們非常有必要學習 Spring Boot 。并且了解其内部實作原理。通過本次分享,您不僅可以學會如何使用 Spring Boot,還可以學習到其内部實作原理,并深入了解:

  1. Spring Boot 項目結構,starter 結構
  2. 常用注解分析
  3. Spring Boot 啟動過程梳理(含:Spring 事件監聽與廣播;自定義事件; SpringFactoriesLoader 工廠加載機制等)
  4. 自定義 starter
  5. 自定義 condition

1. 項目初始化過程

springboot啟動類

springboot啟動類非常簡單,就一句話:

@SpringBootApplication
public class DemoApplication {

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

要想了解springboot的啟動過程,肯定要從這句代碼開始了。

跟進去可以看到有兩步,一個是初始化,一個是run方法的執行:

public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
        return new SpringApplication(sources).run(args);
    }
           

先看初始化方法:

private void initialize(Object[] sources) {
//這裡的sources其實就隻有一個,我們的啟動類DemoApplication.class。
        if (sources != null && sources.length > 0) {
            this.sources.addAll(Arrays.asList(sources));
        }
        //是否是web應用程式。通過判斷應用程式中是否可以加載(class.forname)【"javax.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext"】這兩個類。
        this.webEnvironment = deduceWebEnvironment();
        //設定初始化類:從配置檔案spring.factories中查找所有的key=org.springframework.context.ApplicationContextInitializer的類【加載,初始化,排序】
        //SpringFactoriesLoader:工廠加載機制
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));
                //設定Listeners:從配置檔案spring.factories中查找所有的key=org.springframework.context.ApplicationListener的類.【加載,初始化,排序】
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        //從目前調用棧中,查找主類
        this.mainApplicationClass = deduceMainApplicationClass();
    }
           

SpringFactoriesLoader工廠加載機制

上面代碼中 ,Initializers和Listeners的加載過程都是使用到了

SpringFactoriesLoader

工廠加載機制。我們進入到

getSpringFactoriesInstances

這個方法中:

private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
            Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//擷取META-INF/spring.factories檔案中key為type類型的所有的類全限定名。注意是所有jar包内的。
        Set<String> names = new LinkedHashSet<String>(
                SpringFactoriesLoader.loadFactoryNames(type, classLoader));
//通過上面擷取到的類的全限定名,這裡将會使用Class.forName加載類,并調用構造方法執行個體化類
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
                classLoader, args, names);
//根據類上的org.springframework.core.annotation.Order注解,排序
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }
           

配置檔案中的内容:

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

了解過dubbo的同學會覺得這個非常熟悉,是的,沒錯,和dubbo中的擴充點有異曲同工之妙。

SpringFactoriesLoader.loadFactoryNames(type, classLoader));

方法的執行内容入下,下圖中展示了我們自定義的一個配置類的加載過程(後面自定義starter的實作一節中會講):

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

ApplicationContextInitializer的類圖:

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

下面是listener的類圖(太多了,不全,隻列出了部分)

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

總結初始化initialize過程

  1. 判斷是否是web應用程式
  2. 從所有類中查找META-INF/spring.factories檔案,加載其中的初始化類和監聽類。
  3. 查找運作的主類 預設初始化Initializers都繼承自ApplicationContextInitializer。
Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

預設Listeners有:

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

run方法:

public ConfigurableApplicationContext run(String... args) {
//stopWatch 用于簡單監聽run啟動過程
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        FailureAnalyzers analyzers = null;
        configureHeadlessProperty();
        //擷取監聽器。springboot中隻有一個SpringApplicationRunListener監聽器【參考spring的事件與廣播】
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
        //下面兩句是加載屬性配置,執行完成後,所有的environment的屬性都會加載進來,包括application.properties和外部的屬性配置。
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            Banner printedBanner = printBanner(environment);
            //建立spring容器【Class.forName加載ing執行個體化類:"org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext"或"org.springframework.context.annotation.AnnotationConfigApplicationContext。前者是web應用程式時使用】
            context = createApplicationContext();
            //錯誤分析器
            analyzers = new FailureAnalyzers(context);
            //主要是調用所有初始化類的initialize方法
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            //初始化spring容器【後面寫到】
            refreshContext(context);
            //主要是執行ApplicationRunner和CommandLineRunner的實作類【後面會寫到】
            afterRefresh(context, applicationArguments);
            //通知監聽器
            listeners.finished(context, null);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            return context;
        }
        catch (Throwable ex) {
            handleRunFailure(context, listeners, analyzers, ex);
            throw new IllegalStateException(ex);
        }
    }

private void prepareContext(ConfigurableApplicationContext context,
            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments, Banner printedBanner) {
        context.setEnvironment(environment);//設定環境變量
        postProcessApplicationContext(context);
        //SpringApplication的的初始化器開始工作。主要是循環周遊調用所有Initializers的initialize方法
        applyInitializers(context);
        //目前是空實作
        listeners.contextPrepared(context);
        if (this.logStartupInfo) {
            logStartupInfo(context.getParent() == null);
            logStartupProfileInfo(context);
        }

        // 注冊單例:applicationArguments
        context.getBeanFactory().registerSingleton("springApplicationArguments",
                applicationArguments);
        if (printedBanner != null) {
            context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
        }

        // Load the sources: 隻有一個主類:DemoApplication
        Set<Object> sources = getSources();
        Assert.notEmpty(sources, "Sources must not be empty");
        //加載bean,目前隻有一個主類:DemoApplication
        load(context, sources.toArray(new Object[sources.size()]));
        //廣播容器加載完成事件
        listeners.contextLoaded(context);
    }
           

spring事件

自定義spring事件

spring事件為Bean與bean之間的通訊提供了支援。我們可以發送某個事件,然後通過監聽器來處理該事件。

spring事件繼承自

ApplicationEvent

, 監聽器實作了接口

ApplicationListener<E extends ApplicationEvent>

下面我們自定義一個事件和監聽器

import org.springframework.context.ApplicationEvent;

/**
 * Created by hzliubenlong on 2017/12/17.
 */
@Data
public class MyEvent extends ApplicationEvent{

    private String msg;

    /**
     * Create a new ApplicationEvent.
     *
     * @param source the object on which the event initially occurred (never {@code null})
     */
    public MyEvent(Object source, String msg) {
        super(source);
        this.msg = msg;
    }
}
           
package com.example.demo.event;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

/**
 * Created by hzliubenlong on 2017/12/17.
 */
@Slf4j
@Component
public class MyListener implements ApplicationListener<MyEvent>{

    @Override
    public void onApplicationEvent(MyEvent event) {
        log.info("MyListener收到了MyEvent的消息:{}", event.getMsg());
    }
}
           

編寫一個controller方法:

@Autowired
    ApplicationContext applicationContext;


    @RequestMapping("testPublishMsg")
    public void testPublishMsg(String msg) {
        applicationContext.publishEvent(new MyEvent(this, msg));
    }
           

啟動服務,通路

testPublishMsg

接口,就可以在控制台看到日志列印:

MyListener收到了MyEvent的消息:。。。。。

springboot啟動過程中的事件廣播

SpringApplicationRunListener類圖:

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

上述run過程廣泛應用了spring事件機制(主要是廣播)。上述代碼中首先擷取SpringApplicationRunListeners。這就是在

spring.factories

檔案中配置的所有監聽器。然後整個run 過程使用了listeners的5個方法,每個方法對應一個事件Event:

  • starting() run方法執行的時候立馬執行;對應事件的類型是

    ApplicationStartedEvent

  • environmentPrepared() 

    ApplicationContext

    建立之前并且環境資訊準備好的時候調用;對應事件的類型是

    ApplicationEnvironmentPreparedEvent

  • contextPrepared() 

    ApplicationContext

    建立好并且在source加載之前調用一次;沒有具體的對應事件
  • contextLoaded() 

    ApplicationContext

    建立并加載之後并在refresh之前調用;對應事件的類型是

    ApplicationPreparedEvent

  • finished() run方法結束之前調用;對應事件的類型是

    ApplicationReadyEvent

    ApplicationFailedEven

SpringApplicationRunListeners

SpringApplicationRunListener

的集合,

SpringApplicationRunListener

隻有一個實作類:

EventPublishingRunListener

,在這個實作類中,有一個

SimpleApplicationEventMulticaster

類型的屬性

initialMulticaster

,所有的事件都是通過這個屬性的

multicastEvent

方法廣播出去的。以

environmentPrepared()

方法為例,展示一下環境變了的加載過程:

@Override
    public void environmentPrepared(ConfigurableEnvironment environment) {
        this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
                this.application, this.args, environment));
    }
           

上述廣播了一個

ApplicationEnvironmentPreparedEvent

事件。我們知道所有的事件都會被監聽器捕獲處理,spring的監聽器都是

ApplicationListener

的子類。根據事件的類型,找到處理這個時間的監聽器類

ConfigFileApplicationListener

:

@Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            onApplicationEnvironmentPreparedEvent(
                    (ApplicationEnvironmentPreparedEvent) event);
        }
        if (event instanceof ApplicationPreparedEvent) {
            onApplicationPreparedEvent(event);
        }
    }

    private void onApplicationEnvironmentPreparedEvent(
            ApplicationEnvironmentPreparedEvent event) {
        List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
        postProcessors.add(this);
        AnnotationAwareOrderComparator.sort(postProcessors);
        for (EnvironmentPostProcessor postProcessor : postProcessors) {
            postProcessor.postProcessEnvironment(event.getEnvironment(),
                    event.getSpringApplication());
        }
    }
    //省略部分代碼
@Override
    public void postProcessEnvironment(ConfigurableEnvironment environment,
            SpringApplication application) {
        addPropertySources(environment, application.getResourceLoader());
        configureIgnoreBeanInfo(environment);
        bindToSpringApplication(environment, application);
    }
    //省略部分代碼
/**
     * Add config file property sources to the specified environment.
     * @param environment the environment to add source to
     * @param resourceLoader the resource loader
     * @see #addPostProcessors(ConfigurableApplicationContext)
     */
    protected void addPropertySources(ConfigurableEnvironment environment,
            ResourceLoader resourceLoader) {
        RandomValuePropertySource.addToEnvironment(environment);
//加載屬性配置的最關鍵的一行代碼
        new Loader(environment, resourceLoader).load();
    }
           

ConfigFileApplicationListener

類中可以看到一些其他有用的資訊:[DEFAULTSEARCHLOCATIONS = “classpath:/,classpath:/config/,file:./,file:./config/”][ACTIVEPROFILESPROPERTY = “spring.profiles.active”]等等。

OK,spring的事件機制以及springboot啟動過程中的事件廣播機制講完了。

FailureAnalyzers錯誤分析器

查找并加載spring.factories中所有的org.springframework.boot.diagnostics.FailureAnalyzer。

FailureAnalyzers的類圖:

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

當啟動過程中發生錯誤,會廣播事件并且執行分析器。 目前主要有analyze和report兩個方法,用途是列印日志。

比如:當我們的啟動類

DemoApplication

不是放到最外層的時候,報錯,這就是

FailureAnalyzers

中列印出的結果:

Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled.
2017-12-17 17:04:23.107 ERROR 9356 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Field environmentService in com.example.demo.environment.impl.DemoApplication required a bean of type 'com.example.demo.environment.EnvironmentService' that could not be found.


Action:

Consider defining a bean of type 'com.example.demo.environment.EnvironmentService' in your configuration.


Process finished with exit code 1
           

refresh過程

比較複雜,主要是spring的加載過程,本文的目标是springboot而不是spring,是以不再深入,我會專門寫篇檔案介紹。現在隻需要了解:

  • spring啟動過程
  • bean的加載初始化都是在該方法中完成

afterRefresh與CommandLineRunner、ApplicationRunner

在springboot啟動過程中的run方法的最後,有一句·afterRefresh(context, applicationArguments);·。主要是執行·ApplicationRunner·和·CommandLineRunner·兩個接口的實作類,這兩個類都是在springboot啟動完成後執行的一點代碼,類似于普通bean中的init方法,開機自啟動。 兩者唯一不同是擷取參數方式不同,後面的小例子會講。callRunners的源碼:

List<Object> runners = new ArrayList<Object>();
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        for (Object runner : new LinkedHashSet<Object>(runners)) {
            if (runner instanceof ApplicationRunner) {
                callRunner((ApplicationRunner) runner, args);
            }
            if (runner instanceof CommandLineRunner) {
                callRunner((CommandLineRunner) runner, args);
            }
        }
    }
           

如果有類似的需求:springboot啟動完成後執行一點代碼邏輯,則可以通過實作上述兩個類來完成。下面寫個小例子: 定義4個類:

/**
 * Created by hzliubenlong on 2017/12/17.
 */
@Component
@Order(7)
@Slf4j
public class MyApplicationRunner1 implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        StringBuffer stringBuffer = new StringBuffer();
        args.getOptionNames().forEach(s -> stringBuffer.append(s).append("=").append(args.getOptionValues(s)).append(","));
        log.info("MyApplicationRunner1    "+stringBuffer.toString());
    }
}

/**
 * Created by hzliubenlong on 2017/12/17.
 */
@Component
@Order(9)
@Slf4j
public class MyApplicationRunner2 implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        StringBuffer stringBuffer = new StringBuffer();
        args.getOptionNames().forEach(s -> stringBuffer.append(s).append("=").append(args.getOptionValues(s)).append(","));
        log.info("MyApplicationRunner2    "+stringBuffer.toString());
    }
}

/**
 * Created by hzliubenlong on 2017/12/17.
 */
@Component
@Order(4)
@Slf4j
public class MyCommandLineRunner1 implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        log.info("MyCommandLineRunner1    " + Arrays.asList(args));
    }
}

/**
 * Created by hzliubenlong on 2017/12/17.
 */
@Component
@Order(2)//數字越小,優先級越高
@Slf4j
public class MyCommandLineRunner2 implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        log.info("MyCommandLineRunner2    " + Arrays.asList(args));
    }
}
           

執行結果:

2017-12-17 15:40:13.387  INFO 6244 --- [           main] c.e.demo.runners.MyCommandLineRunner2    : MyCommandLineRunner2    [--a=1, --b=2]
2017-12-17 15:40:13.387  INFO 6244 --- [           main] c.e.demo.runners.MyCommandLineRunner1    : MyCommandLineRunner1    [--a=1, --b=2]
2017-12-17 15:40:13.388  INFO 6244 --- [           main] c.e.demo.runners.MyApplicationRunner1    : MyApplicationRunner1    a=[1],b=[2],
2017-12-17 15:40:13.388  INFO 6244 --- [           main] c.e.demo.runners.MyApplicationRunner2    : MyApplicationRunner2    a=[1],b=[2],
           

結果分析:

  • CommandLineRunner

    ApplicationRunner

    的實作類是在springboot初始化完成後執行的
  • 可以設定執行的順序,數字越小,優先級越高
  • 兩者都可以從外界擷取參數。唯一不同是:

    CommandLineRunner

    的參數類型是字元串數組。而

    ApplicationRunner

    的參數類型是

    ApplicationArguments

    。它可以解析--a=1 --b=2類型的參數為key-value形式。

@order

上述過程中,我們看到了好幾個地方使用到了order,比如

CommandLineRunner

ApplicationRunner

,初始化器Initializer,監聽器Listener。

order比較簡單,主要是為了控制這些實作類的執行順序。規則是數值越小,優先級越高。

在通過spring工廠模式,從

spring.factories

中加載這些實作之後,會通過

AnnotationAwareOrderComparator.sort(instances);

方法來進行排序。

Collections.sort(list, INSTANCE);

//上述第二個參數是比較器,是AnnotationAwareOrderComparator的執行個體,從Order注解中擷取對象的排序數值。
protected Integer findOrder(Object obj) {
        // Check for regular Ordered interface
        Integer order = super.findOrder(obj);
        if (order != null) {
            return order;
        }

        // Check for @Order and @Priority on various kinds of elements
        if (obj instanceof Class) {
            return OrderUtils.getOrder((Class<?>) obj);
        }
        else if (obj instanceof Method) {
            Order ann = AnnotationUtils.findAnnotation((Method) obj, Order.class);
            if (ann != null) {
                return ann.value();
            }
        }
        else if (obj instanceof AnnotatedElement) {
            Order ann = AnnotationUtils.getAnnotation((AnnotatedElement) obj, Order.class);
            if (ann != null) {
                return ann.value();
            }
        }
        else if (obj != null) {
        //真正擷取order的代碼
            order = OrderUtils.getOrder(obj.getClass());
            if (order == null && obj instanceof DecoratingProxy) {
                order = OrderUtils.getOrder(((DecoratingProxy) obj).getDecoratedClass());
            }
        }

        return order;
    }

//跳轉到getOrder方法中,其實作是擷取注解Order的值
public static Integer getOrder(Class<?> type, Integer defaultOrder) {
        Order order = AnnotationUtils.findAnnotation(type, Order.class);
        if (order != null) {
            return order.value();
        }
        Integer priorityOrder = getPriority(type);
        if (priorityOrder != null) {
            return priorityOrder;
        }
        return defaultOrder;
    }
           

總結啟動run過程

  1. 注冊一個StopWatch,用于監控啟動過程
  2. 擷取監聽器SpringApplicationRunListener,用于springboot啟動過程中的事件廣播
  3. 設定環境變量environment
  4. 建立spring容器
  5. 建立FailureAnalyzers錯誤分析器,用于處理記錄啟動過程中的錯誤資訊
  6. 調用所有初始化類的initialize方法
  7. 初始化spring容器
  8. 執行ApplicationRunner和CommandLineRunner的實作類
  9. 啟動完成

springboot中重要的注解

前面講到了@order等注解,下面看一下springboot中最重要的、最常用的幾個注解。

@SpringBootApplication

他是一個組合注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
    @AliasFor(annotation = EnableAutoConfiguration.class, attribute = "exclude")
    Class<?>[] exclude() default {};
    @AliasFor(annotation = EnableAutoConfiguration.class, attribute = "excludeName")
    String[] excludeName() default {};
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    Class<?>[] scanBasePackageClasses() default {};

}
           

@Inherited

Inherited作用是,使用此注解聲明出來的自定義注解,在使用此自定義注解時,如果注解在類上面時,子類會自動繼承此注解,否則的話,子類不會繼承此注解。這裡一定要記住,使用Inherited聲明出來的注解,隻有在類上使用時才會有效,對方法,屬性等其他無效。

參考

關于java 注解中元注解Inherited的使用詳解

@SpringBootConfiguration

源碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
           

@SpringBootConfiguration

繼承自

@Configuration

,二者功能也一緻,标注目前類是配置類,并會将目前類内聲明的一個或多個以@Bean注解标記的方法的執行個體納入到srping容器中,并且執行個體名就是方法名。

@EnableAutoConfiguration

@SuppressWarnings("deprecation")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};

}
           

@EnableAutoConfiguration

用于啟動自動的配置,是springboot的核心注解。上面import了

EnableAutoConfigurationImportSelector

,這個類繼承自

AutoConfigurationImportSelector.AutoConfigurationImportSelector

是關鍵類。

EnableAutoConfigurationImportSelector

類使用了Spring Core包的

SpringFactoriesLoade

r類的

loadFactoryNamesof()

方法。 

SpringFactoriesLoader

會查詢

META-INF/spring.factories

檔案中包含的JAR檔案。

當找到

spring.factories

檔案後,

SpringFactoriesLoader

将查詢配置檔案命名的屬性

org.springframework.boot.autoconfigure.EnableAutoConfiguration

的值。

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

然後在spring啟動過程中的refresh方法中進行真正的bean加載。

@ComponentScan

@ComponentScan

,掃描目前包及其子包下被

@Component

@Controller

@Service

@Repository

注解标記的類并納入到spring容器中進行管理。這也是為什麼我們的啟動類

DemoApplication

要放到項目的最外層的原因。

springboot自動化配置原理及自定義starter

前面的文章已經講了springboot的實作原理,無非就是通過spring的condition條件實作的,還是比較簡單的(感謝spring設計的開放性與擴充性)。

在實際工作過程中會遇到需要自定義starter的需求,那麼我們接下來就自己實作一個starter。

先看一下目錄結構:

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解
  • MyConfig是自定義的配置類
  • HelloService是自定義的bean
  • HelloServiceProperties是自定義的類型安全的屬性配置
  • MEYA-INF/spring.factories檔案是springboot的工廠配置檔案

本項目就是自定義的starter。假設我們這裡需要一些配置項,使用者在使用該starter時,需要在

application.properties

檔案中配置相關屬性。這裡我們使用了

@ConfigurationProperties

來将屬性配置到一個POJO類中。這樣做的好處是:可以檢測資料的類型,并且可以對資料值進行校驗,詳情請參考我的另一篇部落格:

HelloServiceProperties

類内容如下:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * Created by hzliubenlong on 2017/12/13.
 */
@Data
@Component
@ConfigurationProperties(prefix = "hello")
public class HelloServiceProperties {
    private String a;
    private String b;
}
           

這樣使用這個starter的時候,就可以配置

hello.a=**

來設定屬性了。

HelloService

是我們的bean,這裡實作比較簡單,擷取外部的屬性配置,列印一下日志。内容如下:

/**
 * Created by hzliubenlong on 2017/12/12.
 */
@Slf4j
public class HelloService {

    String initVal;

    public void setInitVal(String initVal) {
        this.initVal = initVal;
    }

    public String getInitVal() {
        return initVal;
    }

    public String hello(String name) {
        log.info("initVal={}, name={}", initVal, name);
        return "hello " + name;
    }

}
           

接下來是比較重要的配置類MyConfig,前面已經講過,springboot是通過條件注解來判斷是否要加載bean的,這些内容都是在我們自定義的配置類中來實作:

import com.example.myservice.HelloService;
import com.example.properties.HelloServiceProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(HelloServiceProperties.class)
public class MyConfig {

    @Autowired
    private HelloServiceProperties helloServiceProperties;

    @ConditionalOnProperty(prefix="hello", value="enabled", matchIfMissing = true)
    @ConditionalOnClass(HelloService.class)
    @Bean
    public HelloService initHelloService() {
        HelloService helloService = new HelloService();
        helloService.setInitVal(helloServiceProperties.getA());
        return helloService;
    }
}
           

@Configuration

表明這是一個配置類

@EnableConfigurationProperties(HelloServiceProperties.class)

表示該類使用了屬性配置類

HelloServiceProperties

initHelloService()

方法就是實際加載初始化helloServicebean的方法了,它上面有三個注解:

  1. @ConditionalOnProperty(prefix="hello", value="enabled", matchIfMissing = true): hello.enabled=true

    時才加載這個bean,配置沒有的話,預設為true,也就是隻要引入了這個依賴,不做任何配置,這個bean預設會加載。
  2. @ConditionalOnClass(HelloService.class)

    :當HelloService這個類存在時才加載bean。
  3. @Bean

    :表明這是一個産生bean的方法,改方法生成一個HelloService的bean,交給spring容器管理。

好了,到這裡,我們的代碼已經寫完。根據前面講的springboot的原理我們知道,springboot是通過掃描

MEYA-INF/spring.factories

這個工廠配置檔案來加載配置類的, 是以我們還需要建立這個檔案。其内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.demo.MyConfig
           

上面的

\

是換行符,隻是為了便于代碼的閱讀。通過這個檔案,springboot就可以讀取到我們自定義的配置類MyConfig。 接下來我們隻需要打個jar包即可供另外一個項目使用了。下面貼一下pom.xml的内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>springboot-starter-hello</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot-starter-hello</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-release-plugin</artifactId>
                <version>2.4.2</version>
                <dependencies>
                    <dependency>
                        <groupId>org.apache.maven.scm</groupId>
                        <artifactId>maven-scm-provider-gitexe</artifactId>
                        <version>1.8.1</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.19.1</version>
                <configuration>
                    <skip>false</skip>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.7</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
           

上面定義了一個starter,下面我們将寫一個新的工程,來引用我們自定義的starter。還是先看一下項目目錄:

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

要想引用我們自定義的starter,自然是先引入依賴了:

<dependency>
            <groupId>com.example</groupId>
            <artifactId>springboot-starter-hello</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
           

然後再application.properties檔案中添加如下配置:

hello.enabled=true
hello.a=hahaha
           

好了,現在就可以在項目中注入HelloService使用了。是的,就是那麼簡單.DemoApplication主類如下:

import com.example.demo.environment.EnvironmentService;
import com.example.myservice.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@SpringBootApplication
public class DemoApplication {

    @Autowired
    HelloService helloService;

    @RequestMapping("/")
    public String word(String name) {
        return helloService.hello(name);
    }

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

啟動項目,通路URLhttp://127.0.0.1:8080/?name=hhh:

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

背景列印日志為

initVal=hahaha, name=hhh

其中

hahaha

就是我們沒在配置檔案中配置的屬性值,這句日志是我們上面starter項目中

com.example.myservice.HelloService#hello

方法列印出來的。

自定義Conditional注解

前面已經講過condition的原理。其實自定義一個condition很簡單,隻需要實作

SpringBootCondition

類即可,并重寫

com.example.demo.condition.OnLblCondition#getMatchOutcome

方法。

下面我們簡單寫個執行個體:根據屬性配置檔案中的内容,來判斷是否加載bean。

首先定義一個注解,有兩個内容:一個是屬性檔案的KEY,一個是value。我們要實作的是,屬性檔案的value與指定的value一緻,才建立bean。

package com.example.demo.condition;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnLblCondition.class)
public @interface ConditionalOnLbl {
    String key();
    String value();
}
           

自定義SpringBootCondition的實作類OnLblCondition:

package com.example.demo.condition;

import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Map;

public class OnLblCondition extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ConditionalOnLbl.class.getName());
        Object key = annotationAttributes.get("key");//
        Object value = annotationAttributes.get("value");
        if(key == null || value == null){
            return new ConditionOutcome(false, "error");
        }

        //擷取environment中的值
        String key1 = context.getEnvironment().getProperty(key.toString());
        if (value.equals(key1)) {//如果environment中的值與指定的value一緻,則傳回true
            return new ConditionOutcome(true, "ok");
        }
        return new ConditionOutcome(false, "error");
    }
}
           

然後随便定義一個類:

@Slf4j
public class MyConditionService {
    public void say() {
        log.info("MyConditionService init。");
    }
}
           

好了,condition已經自定義完成。接下來就是如何使用了:

@Configuration
@Slf4j
public class MySpringBootConfig {
    @ConditionalOnLbl(key = "com.lbl.mycondition", value = "lbl")
    @ConditionalOnClass(MyConditionService.class)
    @Bean
    public MyConditionService initMyConditionService() {
        log.info("MyConditionService已加載。");
        return new MyConditionService();
    }
           

要想加載

MyConditionService

的bean到spring容器中,需要滿足以下兩個條件:

  1. 屬性檔案中配置了

    com.lbl.mycondition=lbl

  2. 可以加載

    MyConditionService

好了,讓我們在

application.properties

檔案中添加配置

com.lbl.mycondition=lbl

 啟動項目,可以看到日志: 

MyConditionService

已加載。 把

application.properties

檔案中的

com.lbl.mycondition

去掉,或者更改個值,則上述日志不會列印,也就是不會建立

MyConditionService

這個bean .

spring @Conditional注解

前面講了springboot的實作基礎是spring的@Conditional注解。介紹原理前我們來看看怎麼用。後面介紹其原理。

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

我們實作這麼一個小功能:**根據不同的環境,執行個體化不同的bean。 ** springboot通常都是通過-Dspring.profiles.active=dev來區分環境的,如果我們想實作線上的代碼邏輯與開發或者測試環境不同,那麼這是一個解決方案。

使用java的多态,先定義一個接口:

public interface EnvironmentService {

    void printEnvironment();
}
           

然後定義兩個不同環境的實作類,内容比較簡單,隻是輸出一句log。

@Slf4j
public class ProdService  implements EnvironmentService {

    @Override
    public void printEnvironment() {
        log.info("我是生産環境。");
    }
}
           
@Slf4j
public class DevService implements EnvironmentService{

    @Override
    public void printEnvironment() {
        log.info("我是開發環境。");
    }
}
           

好了,,準備工作已經做完,接下來編寫配置類,使用spring的@Conditional注解來根據不同的環境配置加載不同的類:

import com.example.demo.environment.impl.DevService;
import com.example.demo.environment.impl.ProdService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * 根據不同的環境,初始化不同的bean
 */
@Configuration
@Slf4j
public class MySpringBootConfig {

    @ConditionalOnExpression("#{'${spring.profiles.active}'.equals('dev')}")//使用了spring的SPEL表達式
    @ConditionalOnClass(DevService.class)
    @Bean
    public DevService initDevService() {
        log.info("DevService已加載。");
        return new DevService();
    }

    @ConditionalOnExpression("#{'${spring.profiles.active}'.equals('prod')}")
    @ConditionalOnClass(ProdService.class)
    @Bean
    public ProdService initProdService() {
        log.info("ProdService已加載。");
        return new ProdService();
    }
}
           

官方的

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

​condition有很多: 這裡我們使用的是@ConditionalOnExpression。它根據SPEL表達式傳回的結果作為條件判斷。 這裡判斷條件為:spring.profiles.active=dev時,建立DevService。為prod時,建立ProdService。

接下來看如何使用:

import com.example.demo.environment.EnvironmentService;
import com.example.myservice.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@SpringBootApplication
public class DemoApplication {

    @Autowired
    HelloService helloService;
    @Autowired
    EnvironmentService environmentService;

    @Value("#{'${spring.profiles.active}'.equals('dev')}")
    String springProfilesActive;

    @RequestMapping("/")
    public String word(String name) {
        environmentService.printEnvironment();
        log.info("springProfilesActive={}", springProfilesActive);
        return helloService.hello(name);
    }

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

很簡單,隻需要注入

EnvironmentService

即可。注意IDEA可能會提示錯誤,因為這個接口有兩個實作類。不過不用去管他,因為我們通過condition隻執行個體化了一個bean。

Spring Boot 1.5.3 源碼深入分析1. 項目初始化過程springboot中重要的注解springboot自動化配置原理及自定義starter自定義Conditional注解spring @Conditional注解備注:springboot的核心注解

運作結果:

我是開發環境。
springProfilesActive=true
initVal=hahaha, name=hhh
           

将配置修改為prod後:

我是生産環境。
springProfilesActive=true
initVal=hahaha, name=hhh
           

使用非常簡單,那麼

@Conditional

是怎麼實作的呢?

官方文檔的說明是“隻有當所有指定的條件都滿足是,元件才可以注冊”。主要的用處是在建立bean時增加一系列限制條件。 他的核心類是:

public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
           

所有的condition都是Condition接口的實作類,條件判斷是通過matches方法傳回的布爾值來判斷的。

@ConditionalOnExpression

注解為例:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnExpressionCondition.class)
public @interface ConditionalOnExpression {

    /**
     * The SpEL expression to evaluate. Expression should return {@code true} if the
     * condition passes or {@code false} if it fails.
     * @return the SpEL expression
     */
    String value() default "true";
}
           

ConditionalOnExpression

注解上添加了另一個注解Conditional,指明是哪個condition類處理改注解。 我們打開

OnExpressionCondition

這個類:

@Override
    public ConditionOutcome getMatchOutcome(ConditionContext context,
            AnnotatedTypeMetadata metadata) {
            //擷取到我們配置的EL表達式的值
        String expression = (String) metadata
                .getAnnotationAttributes(ConditionalOnExpression.class.getName())
                .get("value");
        expression = wrapIfNecessary(expression);
        String rawExpression = expression;
        expression = context.getEnvironment().resolvePlaceholders(expression);
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        BeanExpressionResolver resolver = (beanFactory != null)
                ? beanFactory.getBeanExpressionResolver() : null;
        BeanExpressionContext expressionContext = (beanFactory != null)
                ? new BeanExpressionContext(beanFactory, null) : null;
        if (resolver == null) {
            resolver = new StandardBeanExpressionResolver();
        }
        boolean result = (Boolean) resolver.evaluate(expression, expressionContext);
        return new ConditionOutcome(result, ConditionMessage
                .forCondition(ConditionalOnExpression.class, "(" + rawExpression + ")")
                .resultedIn(result));
    }
           

有這麼一個方法,擷取到我們設定的EL表達式的值後,進行一些處理,比如管道

environment

中的值,然後計算整體的結果。 其中關鍵的是

boolean result = (Boolean) resolver.evaluate(expression, expressionContext);

 這麼一行代碼,通過springEL計算最終結果。

至此,spring的condition算是介紹完了,我們可以通過實作

org.springframework.context.annotation.Condition#matches

來自定義condition。

當然抛開@Condition注解,實作不同環境加載不同的類,既可以使用

ConditionalOnExpression

也可以使用

@Profile

注解

兩種都可以實作不同環境加載不同的類,寫法不同,但是他們的實作原理都是一樣的,都是擷取環境變量

spring.profiles.active

的值,與value進行比較。讀者可以去看一下

ProfileCondition

源碼
@Bean
    @Profile("dev")
    public DevService initDevService1() {
        log.info("DevService已加載---Profile");
        return new DevService();
    }

    @Bean
    @Profile("prod")
    public ProdService initProdService1() {
        log.info("ProdService已加載---Profile");
        return new ProdService();
    }
           

備注:springboot的核心注解

spring.factories檔案裡每一個xxxAutoConfiguration檔案一般都會有下面的條件注解:

@ConditionalOnBean:當容器裡有指定Bean的條件下

@ConditionalOnClass:當類路徑下有指定類的條件下

@ConditionalOnExpression:基于SpEL表達式作為判斷條件

@ConditionalOnJava:基于JV版本作為判斷條件

@ConditionalOnJndi:在JNDI存在的條件下差在指定的位置

@ConditionalOnMissingBean:當容器裡沒有指定Bean的情況下

@ConditionalOnMissingClass:當類路徑下沒有指定類的條件下

@ConditionalOnNotWebApplication:目前項目不是Web項目的條件下

@ConditionalOnProperty:指定的屬性是否有指定的值

@ConditionalOnResource:類路徑是否有指定的值

@ConditionalOnSingleCandidate:當指定Bean在容器中隻有一個,或者雖然有多個但是指定首選Bean

@ConditionalOnWebApplication:目前項目是Web項目的條件下。

上面@ConditionalOnXXX都是組合@Conditional元注解,使用了不同的條件Condition