關於SpringBoot的自動配置和啟動過程


一、簡介

Spring Boot簡化了Spring應用的開發,采用約定大於配置的思想,去繁從簡,很方便就能構建一個獨立的、產品級別的應用。

1.傳統J2EE開發的缺點

開發笨重、配置繁多復雜、開發效率低下、部署流程復雜、第三方技術集成難度大。

2.SpringBoot的優點

  • 快速重建獨立運行的Spring項目以及與主流框架集成。
  • 使用嵌入式的Servlet容器,應用無需打成WAR包
  • starters自動依賴與版本控制
  • 大量的自動配置、簡化開發,也可以修改其默認值
  • 無需配置XML,無代碼生成
  • 准生產環境的運行時應用監控
  • 與雲計算的天然繼承

3.SpringBoot helloworld說明

1.starters

  • SpringBoot為我們提供了簡化企業級開發絕大多數場景的starters pom(啟動器),只要引入了相應場景的starters pom,相關技術的絕大部分配置將會消除(字段配置),從而簡化我們的開發。業務中我們就會使用到SpringBoot為我們字段配置的Bean。
  • 這些starters幾乎涵蓋了javaee所有常用場景,SpringBoot對這些場景依賴的jar也做了嚴格的測試和版本控制。
  • spring-boot-dependencies里面定義了jar包的版本。

2.入口類和@SpringBootApplication

  • 程序從main方法開始運行。
  • 使用SpringApplication.run()加載主程序類
  • 主程序類需標注@SpringBootApplication
  • @EnableAutoConfiguration是核心注解
  • @Import導入所有的自動配置場景
  • @AutoConfigurationPackage定義默認的包掃描規則。
  • 程序啟動掃描主程序類所在的包以及下面所有子包的組件

3.自動配置

自動配置xxxAutoConfiguration

  • SpringBoot中存現大量的這些類,這些類的作用就是幫我們進行自動裝配
  • 它會將這個場景需要的所有組件都注冊到容器中,並配置好
  • 他們在類路徑下的META-INF/spring.factories文件中
  • spring-boot-autoconfigure.jar中包含了所有場景的字段配置類代碼
  • 這些自動配置類是SpringBoot進行自動裝配的關鍵。

二、SpringBoot配置

1.配置文件

  • SpringBoot使用一個全局的配置文件。配置文件名是固定的。
    -application.properties或者application.yml

  • 配置文件放在src/main/resources目錄或者類路徑/config下。

  • 全局配置文件的作用是對一些默認配置進行修改

2.配置文件值注入

  • @Value和@ConfigurationProperties為屬性注入值進行對比
對比點 @ConfigurationProperties @Value
功能 批量注入配置文件中的屬性 一個個指定
松散綁定(松散語法) 支持 不支持
SpEL 不支持 支持
JSR303數據校驗 支持 不支持
復雜類型封裝 支持 不支持
  • 屬性名匹配規則
    -person.firstName 使用標准方式
    -person.first-name 使用-
    -person.first_name 使用_
    -PERSON_FIRST_NAME 推薦系統屬性使用這種寫法

  • @PropertySource
    加載指定的配置文件

  • ConfigurationProperties
    -與@Bean結合為屬性賦值
    -與@PropertySource(只能用於properties文件)結合讀取指定文件。

  • ConfigurationProperties Validation
    -支持JSR303進行配置文件值校驗。


@Component
@PropertySource(value={"classpath:person.properties"})
@ConfigurationProperties(prefix="person")
@Validated
public class Person{
    @Email
    @Value("${person.email}")
    private String email;
}
  • ImportResource讀取外部配置文件

3.配置文件占位符

  • RandomValuePropertySource
    配置文件中可以使用隨機數
    -${random.value}
    -${random.int}
    -${random.long}
    -${random.int(10)}
    -${random.int[1024,65536]}

  • 屬性配置占用符
    -可以在配置文件中引用前面配置過的屬性(Y優先級前面配置過的這里都可以使用)。
    -${app.name:默認值}來指定找不到屬性時的默認值。

app.name=MyApp    
app.description=${app.name} is a SpringBoot Application

4.profile

profile是Spring對不同環境提供不同配置功能的支持,可以通過激活指定參數的方式快速切換環境。

1.多profile文件形式

  • 格式:application-{profile}.properties/yml
    application-dev.properties、application-prod.properties

2.多profile文檔塊模式

spring.profiles.active=prod #激活指定配置
spring.profiles=prod
server.port=80
# default表示未指定時的默認配置
spring.profiles=default
server.port=8080

3.激活方式

  • 命令行:--spring.profiles.active=dev
  • 配置文件:spring.profiles.active=dev
  • jvm參數:-Dspring.profiles.active=dev

5.配置文件加載位置

SpringBoot啟動會掃描一下位置的application.properties或者application.yml文件作為SpringBoot的默認配置文件。
- file:./config/
- file:./
- classpath:/config/
-classpath:/
-以上是按照優先級從高到低的順序,所有位置的文件都會被加載,高優先級配置內容會覆蓋低優先級配置內容。
-可以通過配置spring.config.location來改變默認配置。

6.外部配置加載順序

  1. 命令行參數
  2. 來自java:comp/env的JNDI屬性
  3. Java系統屬性(System.getProperties())
  4. 操作系統環境變量
  5. RandomValuePropertySource配置的random.*屬性值
  6. jar包外部的application-{profile}.properties或application.yml(帶spring.profile)配置文件
  7. jar包內部的application-{profile}.properties或application.yml(帶spring.profile)配置文件
  8. jar包外部的application.properties或application.yml(不帶spring.profile)配置文件
  9. jar包內部的application.properties或application.yml(不帶spring.profile)配置文件
  10. @Configuration注解類上的@PropertySource。
  11. 通過SpringApplication.setDefaultproperties指定的默認屬性。

7.自動配置原理

1.SpringBoot啟動的時候加載主配置類,開啟了自動配置功能@EnableAutoConfiguration

2.@EnableAutoConfiguration作用

  • 利用EnableAutoConfigurationImportSelector給容器中導入一些組件。
  • 將類路徑小META-INF/spring.factories里面配置的所有EnableAutoConfiguration的值加入到了容器中。

3.@Conditional派生注解

@Conditional擴展注解 作用(判斷是否滿足當期指定條件)
@ConditionalOnJava 系統的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean
@ConditionalOnMissingBean 容器中不存在指定Bean
@ConditionalOnExpression 滿足SpEL表達式指定
@ConditionalOnClass 系統中有指定的類
@ConditionalOnMissingClass 容器中沒有指定類
@ConditionalOnSingleCandidate 容器中只有一個指定的Bean,或者這個Bean是首選Bean
@ConditionalOnProperty 系統中指定的屬性是否有指定的值
@ConditionalOnResource 類路徑下是否存在指定資源文件
@ConditionalOnWebApplication 當前是web環境
@ConditionalOnNotWebApplication 當前不是web環境
@ConditionalOnJndi JNDI存在指定項
  • 作用:必須是@Conditional指定的條件成立,才給容器中添加組件,配置配里面的所有內容才生效。

三、SpringBoot與日志

1.日志框架

市場上存在非常多的日志框架,JUL(java.util.logging)、JCL(Apache Commons Logging)、Log4J、Log4J2、Logback、SLF4j、jboss-logging等。

  • SpringBoot早框架內部使用JCL。spring-boot-starter-logging采用了slf4j+logback的形式,SpringBoot也能自動配置(jul、log4j2、logback)並簡化配置。
日志門面 日志實現
JCL、SLF4J、jboss-logging log4j、JUL、Log4j2、Logback
日志系統 配置文件
Logback logback-spring.xml、logback-spring.groovy、logback.xml或logback.groovy
Log4j2 log4j2-spring.xml、log4j2.xml
JUL logging.properties

  • 總結:
    1.SpringBoot底層也是使用slf4j+logback的方式進行日志記錄。
    2.SpringBoot也把其他的日志都替換成了slf4j。
    3.如果要引入其他日志框架,要排除Spring框架的commons-logging依賴。

四、Web開發

1.SpringBoot對靜態資源的映射規則

  • 所有/webjars/**,都去classpath:/META-INF/resources/webjars/ 下面找資源。
  • "/**" 訪問當前項目的任何資源,都去(靜態資源的文件夾)找映射。
  • 歡迎頁;靜態資源文件夾下的所有index.html頁面,被"/**" 映射。
  • 所有的 **/favicon.ico都是在靜態資源文件下找。

2.SpringMVC自動配置

1.SpringMVC auto-configuration

SpringBoot對SpringMVC的默認配置(WebMvcAutoConfiguration)如下:

  • 包含了ContentNegotiatingViewResolver和BeanNameViewResolver。
    • 自動配置了ViewResolver
    • ContentNegotiatingViewResolver:組合所有的視圖解析器
- 支持靜態資源,包括支持Wenjars
- 靜態首頁訪問
- 支持favicon.ico
- 自動注冊了Converter、GenericConverter、Formatter。  
     - Converter:轉換器。
     - Formatter:格式化器
  • 支持HttpMessageConverters

    • HttpMessageConverter:SpringMVC用來轉換Http請求和響應。
    • HttpMessageConverters:從容器中確定,獲取所有的HttpMessageConverter;
  • 自動注入MessageCodesResolver,定義錯誤碼生成規則。

  • 自動使用ConfigurableWebBindingInitializer。

2.擴展SpringMVC

編寫一個配置類(@Configuration),是WebMvcConfigurerAdapter類型,不能標注@EnableWebMvc注解

@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/desperado").setViewName("success");
    }
}

原理

  1. WebMvcAutoConfiguration是SpringMVC的自動配置類。
  2. 在做其他自動配置時會導入。
  3. 容器中所有的WebMvcConfigurer都會一起被注冊。
  4. 我們自定義的配置類也會被調用。

3.全面接管SpringMVC

如果想要使SpringMVC的自動配置失效,只需要在我們自定義的配置類中添加@EnableWebMvc注解即可。

@EnableWebMvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/desperado").setViewName("success");
    }
}

原理

  1. @EnableWebMvc的注解
@Import(DelegatingWebMvcConfiguation.class)
public @interface EnableWebMvc{}
  1. DelegatingWebMvcConfiguation
@Configuration
public class DelegatingWebMvcConfiguation extend WebMvcConfigurationSupport{}
  1. WebMvcAutoConfiguration
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({Servlet.class,DispatcherServlet.class,
                      WebMvcConfigurerAdapter.class})
//容器中沒有這個組件,這個自動配置類才會生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class,
                ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration{}
  1. @EnableWebMvc會將WebMvcConfigurationSupport組件導入進來。
  2. 導入的WebMvcConfigurationSupport只是SpringMVC最基本的功能。

4.修改默認配置

  1. SpringBoot在自動配置很多組件的時候,顯卡容器中有沒有用戶自己配置的(@Bean 、@Component),如果有就用用戶配置的,如果沒有,才會進行自動配置;如果某些組件可以有多個,將用戶配置的和自己默認的組合起來。
  2. 在SpringBoot中有許多的xxxConfigurer幫助我們進行擴展配置
  3. 在SpringBoot中有許多的xxxCustomizer幫助我們進行定制配置

5.默認訪問首頁

使用自定義WebMvcConfigurationAdapter進行配置

//使用WebMvcConfigurationAdapter可以擴展SpringMVC的功能
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //瀏覽器發送/desperado 請求來到success
        registry.addViewController("/desperado").setViewName("success");
    }

    //所有的webMvcConfigurerAdapter組件都會一起起作用
    @Bean   //將組件注冊到容器
    public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){
        WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() {
            @Override
            public void addViewControllers(ViewControllerRegistry registry) {
                //配置默認路徑的頁面
                registry.addViewController("/").setViewName("login");
                registry.addViewController("/index.html").setViewName("login");
            }
        };
        return adapter;
    }
}

6.國際化

1.編寫國際化配置文件
編寫不同語言的配置文件,比如login.properties、login_en_US.properties、login_zh_CN.properties等。
2. SpringBoot自動配置好了管理國際化資源文件的組件。

@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
    private static final Resource[] NO_RESOURCES = new Resource[0];

    public MessageSourceAutoConfiguration() {
    }

    @Bean
    @ConfigurationProperties(
        prefix = "spring.messages"
    )
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }

    @Bean
    public MessageSource messageSource(MessageSourceProperties properties) {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(properties.getBasename())) {
            //設置國際化資源文件的基礎名(去掉語言國家代碼)
      
                messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
        }

        if (properties.getEncoding() != null) {
            messageSource.setDefaultEncoding(properties.getEncoding().name());
        }

        messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
        Duration cacheDuration = properties.getCacheDuration();
        if (cacheDuration != null) {
            messageSource.setCacheMillis(cacheDuration.toMillis());
        }

        messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
        messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
        return messageSource;
    }

原理
根據請求頭帶來的區域信息獲取Locale進行國際化。

五、錯誤處理機制

1.默認的錯誤處理機制

  1. 瀏覽器,默認返回一個默認的錯誤頁面。
  2. 其他客戶端,默認響應一個json數據。

原理

  1. 在DefaultErrorAttributes中獲取錯誤頁面的信息
public class DefaultErrorAttributes implements ErrorAttributes {
 
    //獲取錯誤頁面的信息
    public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        errorAttributes.put("path", request.path());
        Throwable error = this.getError(request);
        HttpStatus errorStatus = this.determineHttpStatus(error);
        errorAttributes.put("status", errorStatus.value());
        errorAttributes.put("error", errorStatus.getReasonPhrase());
        errorAttributes.put("message", this.determineMessage(error));
        this.handleException(errorAttributes, this.determineException(error), includeStackTrace);
        return errorAttributes;
    }
}
  1. 在BasicErrorController中處理/error請求
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
   
    //產生html類型的數據,瀏覽器發送的請求來打這個方法處理
    @RequestMapping(
        produces = {"text/html"}
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
       //去哪個頁面作為錯誤頁面。包含頁面地址和頁面內容
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

   //產生json數據,其他客戶端來到這個方法處理
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = this.getStatus(request);
        return new ResponseEntity(body, status);
    }
}
  1. ErrorPageCustomizer進行錯誤配置
public class ErrorProperties {
    @Value("${error.path:/error}")
    private String path = "/error";
    private boolean includeException;
    private ErrorProperties.IncludeStacktrace includeStacktrace;
    private final ErrorProperties.Whitelabel whitelabel;
}
  1. ErrorMvcAutoConfiguration生成錯誤頁面
public class ErrorMvcAutoConfiguration {
    
    private static class StaticView implements View {
        private static final Log logger = LogFactory.getLog(ErrorMvcAutoConfiguration.StaticView.class);

        private StaticView() {
        }

        public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
            if (response.isCommitted()) {
                String message = this.getMessage(model);
                logger.error(message);
            } else {
                StringBuilder builder = new StringBuilder();
                Date timestamp = (Date)model.get("timestamp");
                Object message = model.get("message");
                Object trace = model.get("trace");
                if (response.getContentType() == null) {
                    response.setContentType(this.getContentType());
                }

                builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>");
                if (message != null) {
                    builder.append("<div>").append(this.htmlEscape(message)).append("</div>");
                }

                if (trace != null) {
                    builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>");
                }

                builder.append("</body></html>");
                response.getWriter().append(builder.toString());
            }
        }

        private String htmlEscape(Object input) {
            return input != null ? HtmlUtils.htmlEscape(input.toString()) : null;
        }

        private String getMessage(Map<String, ?> model) {
            Object path = model.get("path");
            String message = "Cannot render error page for request [" + path + "]";
            if (model.get("message") != null) {
                message = message + " and exception [" + model.get("message") + "]";
            }

            message = message + " as the response has already been committed.";
            message = message + " As a result, the response may have the wrong status code.";
            return message;
        }

        public String getContentType() {
            return "text/html";
        }
    }

5.DefaultErrorViewResolver解析頁面

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
  
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
        }

        return modelAndView;
    }

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //默認去找一個頁, error/404
        String errorViewName = "error/" + viewName; 
        //模板引擎可以解析這個頁面地址就要模板引擎解析
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
        //模板引擎可用的情況下返回到errorViewName指定的視圖地址
        //模板引擎不可以,就在靜態資源文件夾下找對應的頁面
        return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
    }

2.錯誤頁面的優先級(自定義錯誤頁面)

  1. 在模板引擎的情況下,error/狀態碼(將錯誤頁面命名為 錯誤狀態碼.html 放在模板引擎文件夾里面的error文件夾下),發生此狀態碼的錯誤就會來到對應的頁面;
  2. 沒有模板引擎(模板引擎找不到錯誤頁面),就在靜態資源文件夾下找。
  3. 以上都沒有錯誤頁面,就是默認來到SpringBoot默認的錯誤提示頁面。

3.如何定制錯誤的json數據

  1. 自定義異常處理&返回定制json數據(沒有自適應效果)
@ControllerAdvice
public class MyExceptionHandler  {
    
    @ResponseBody
    @ExceptionHandler(CustomException.class)
    public Map<String,Object> handleException(Exception e){
        HashMap<String, Object> map = new HashMap<>();
        map.put("code","錯誤信息");
        map.put("message",e.getMessage());
        return map;
    }
}

2.轉發到/error進行自適應響應效果處理。

@ControllerAdvice
public class MyExceptionHandler  {

    @ResponseBody
    @ExceptionHandler(CustomException.class)
    public String handleException(Exception e, HttpServletRequest request){
        HashMap<String, Object> map = new HashMap<>();
        //傳入我們自己的錯誤狀態碼,否則就不會進入定制錯誤頁面的解析流程
        request.setAttribute("java.servlet.error.status_code","500");
        map.put("code","錯誤信息");
        map.put("message",e.getMessage());
        //轉發到/error
        return "forward:/error";
    }
}

4.將定制的數據發送出去

出現錯誤之后,回來到/error請求,會被BasicErrorController處理,響應出去可以獲取的數據是由getErrorAttributes得到的。

  1. 編寫一個ErrorController的實現類(或者是編寫AbstractErrorController的子類),放到容器中。
  2. 頁面上能用的數據,或者是json返回能用的數據都是通過errorAttributes.getErrorAttributes得到;容器中DefaultErrorAttributes.getErrorAttributes()默認進行數據處理.
//給容器中加入自定義的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        //獲取ErrorAttributes的map
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        //加入自己屬性字段
        map.put("name","desperado");
        return map;
    }
}

六、配置嵌入式Servlet容器

SpringBoot默認使用Tomcat作為內嵌的Servlet容器

1.修改Servlet容器的配置

在配置文件application文件中修改和server有關的配置。

server.port=8081
server.context_path=/crud
server.tomcat.uri-encoding=utf-8

2. 定制Servlet容器的相關配置

編寫一個EmbeddedServletContainerCustomizer(2.x中使用WebServerFactoryCustomizer),來修改Servlet容器的配置。

@Bean
	public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
		return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>(){
			@Override
			public void customize(ConfigurableWebServerFactory factory) {
				 factory.setPort(8081);
			}
		};
	}

3.注冊Servlet三大組件

由於SpringBoot是默認以jar包的方式啟動內嵌的Servlet容器來啟動SpringBoot的web應用,沒有web.xml文件。所以注冊Servlet、Filter、Listener的方式也不同

1. 注入Servlet

    @Bean
	public ServletRegistrationBean<MyServlet> myServlet(){
		ServletRegistrationBean<MyServlet> registrationBean = 
				new ServletRegistrationBean<>(new MyServlet(), "/myServlet");
		return registrationBean;
	}

2. 注入Filter

	@Bean
	public FilterRegistrationBean<MyFilter> myFilter(){
		FilterRegistrationBean<MyFilter> registrationBean = new FilterRegistrationBean<>();
		registrationBean.setFilter(new MyFilter());
		registrationBean.setUrlPatterns(Arrays.asList("/hello","/myServlet"));
		return registrationBean;
	}

3. 注入Listener

	@Bean
	public ServletListenerRegistrationBean<MyListener> myListener(){
		ServletListenerRegistrationBean<MyListener> registrationBean =
				new ServletListenerRegistrationBean<>(new MyListener());
		return registrationBean;
	}

4.替換為其他嵌入式Servlet容器

替換為其他的Servlet非常簡單,只需要在pom中引入其依賴,然后排除tomcat的依賴即可.

      <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<exclusions>
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-tomcat</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

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

5. 嵌入式Servlet容器自動配置原理

  1. EmbeddedServletContainerAutoConfiguration(2.x對應ServletWebServerFactoryConfiguration):嵌入式容器的自動配置
@Configuration
class ServletWebServerFactoryConfiguration {
    ServletWebServerFactoryConfiguration() {
    }

    @Configuration
    @ConditionalOnClass({Servlet.class, Undertow.class, SslClientAuthMode.class})
    @ConditionalOnMissingBean(
        value = {ServletWebServerFactory.class},
        search = SearchStrategy.CURRENT
    )
    public static class EmbeddedUndertow {
        public EmbeddedUndertow() {
        }

        @Bean
        public UndertowServletWebServerFactory undertowServletWebServerFactory() {
            return new UndertowServletWebServerFactory();
        }
    }

    @Configuration
    @ConditionalOnClass({Servlet.class, Server.class, Loader.class, WebAppContext.class})
    @ConditionalOnMissingBean(
        value = {ServletWebServerFactory.class},
        search = SearchStrategy.CURRENT
    )
    public static class EmbeddedJetty {
        public EmbeddedJetty() {
        }

        @Bean
        public JettyServletWebServerFactory JettyServletWebServerFactory() {
            return new JettyServletWebServerFactory();
        }
    }

    @Configuration
   //判斷當前是否引入了tomcat依賴
    @ConditionalOnClass({Servlet.class, Tomcat.class, UpgradeProtocol.class}) 
    ///判斷當前容器沒有用戶自己定義ServletWebServerFactory:
    //嵌入式的Servlet容器工廠;作用:創建嵌入式的Servlet容器

    @ConditionalOnMissingBean(
        value = {ServletWebServerFactory.class},
        search = SearchStrategy.CURRENT
    )
    public static class EmbeddedTomcat {
        public EmbeddedTomcat() {
        }

        @Bean
        public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
            return new TomcatServletWebServerFactory();
        }
    }
}
  1. 嵌入式Servlet容器工廠

  1. 嵌入式的Servlet容器

4.以tomcat為例

public WebServer getWebServer(ServletContextInitializer... initializers) {
        Tomcat tomcat = new Tomcat();
        File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        Connector connector = new Connector(this.protocol);
        tomcat.getService().addConnector(connector);
        this.customizeConnector(connector);
        tomcat.setConnector(connector);
        tomcat.getHost().setAutoDeploy(false);
        this.configureEngine(tomcat.getEngine());
        Iterator var5 = this.additionalTomcatConnectors.iterator();

        while(var5.hasNext()) {
            Connector additionalConnector = (Connector)var5.next();
            tomcat.getService().addConnector(additionalConnector);
        }

        this.prepareContext(tomcat.getHost(), initializers);
        return thisb.getTomcatWebServer(tomcat);
    }

  1. 容器中導入 WebServerFactoryCustomizerBeanPostProcessor

public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
  
     //初始化之前   
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        //如果當前初始化的是一個WebServerFactory類型的組件
        if (bean instanceof WebServerFactory) {
            this.postProcessBeforeInitialization((WebServerFactory)bean);
        }

        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
       //  獲取所有的定制器,調用每一個定制器的customize方法來給servlet容器進行屬性賦值
        ((Callbacks)LambdaSafe.callbacks(WebServerFactoryCustomizer.class, this.getCustomizers(), webServerFactory, new Object[0]).withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)).invoke((customizer) -> {
            customizer.customize(webServerFactory);
        });
    }

    private Collection<WebServerFactoryCustomizer<?>> getCustomizers() {
        if (this.customizers == null) {
            // 定制Servlet容器,給容器中可以添加一個WebServerFactoryCustomizer類型的組件
            this.customizers = new ArrayList(this.getWebServerFactoryCustomizerBeans());
            this.customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
            this.customizers = Collections.unmodifiableList(this.customizers);
        }

        return this.customizers;
    }

    private Collection<WebServerFactoryCustomizer<?>> getWebServerFactoryCustomizerBeans() {
      //從容器中獲取所有這個類型的組件:WebServerFactoryCustomizer
        return this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false).values();
    }
}

總結

  1. SpringBoot根據導入的依賴情況,給容器中添加相應的WebServerFactory【TomcatServletWebServerFactory】。
  2. 容器中某個組件要創建對象就會使用WebServerFactoryCustomizerBeanPostProcessor后置處理器,只要是嵌入式的Servlet工廠,后置處理器就會進行處理。
  3. 后置處理器從容器中獲取所有的WebServerFactoryCustomizer,調用定制器的定制方法

6.嵌入式Servlet容器啟動過程

  1. SpringBoot啟動運行run方法。
  2. 調用refreshContext(context);刷新IOC容器【創建IOC容器,並初始化容器,創建容器中的每一個組件】;如果是web應用創建AnnotationConfigServletWebServerApplicationContext,如果是reactive應用創建AnnotationConfigReactiveWebServerApplicationContext,否則創建AnnotationConfigApplicationContext。
 protected ConfigurableApplicationContext createApplicationContext() {
        Class<?> contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                switch(this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
                    break;
                case REACTIVE:
                    contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
                    break;
                default:
                    contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
                }
            } catch (ClassNotFoundException var3) {
                throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
            }
        }

        return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
    }
  1. 調用refresh(context);刷新上面創建好的IOC容器
 public void refresh() throws BeansException, IllegalStateException {
        Object var1 = this.startupShutdownMonitor;
        synchronized(this.startupShutdownMonitor) {
            //准備刷新的context
            this.prepareRefresh();
            //調用子類去刷新內部的實例工廠
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            //准備在這個context中要使用的實例工廠
            this.prepareBeanFactory(beanFactory);

            try {
                
                // 允許在上下文子類中對bean工廠進行后置處理。
                this.postProcessBeanFactory(beanFactory);
                //在context中調用注冊為bean的工廠處理器。
                this.invokeBeanFactoryPostProcessors(beanFactory);
                //注冊攔截bean創建的bean處理器。      
                this.registerBeanPostProcessors(beanFactory);
                //初始化此context的消息源
                this.initMessageSource();
                //初始化此上下文的事件多播器。
                this.initApplicationEventMulticaster();
                //在特定的context子類中初始化其他特殊bean。
                this.onRefresh();
                // 檢查監聽器bean並注冊它們。
                this.registerListeners();
                // 實例化所有剩余(非延遲初始化)單例。
                this.finishBeanFactoryInitialization(beanFactory);
                //最后一步:發布相應的事件。
                this.finishRefresh();
            } catch (BeansException var9) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
                }

                //摧毀已經創建的單例以避免占用資源。
                this.destroyBeans();
                //重置 ‘active’ 標志
                this.cancelRefresh(var9); 
                //Propagate exception to caller.
                throw var9;
            } finally {
              //從我們開始,重置Spring核心中的常見內省緩存
              //可能不再需要單例bean的元數據了...
                this.resetCommonCaches();
            }

        }
    }
  1. 調用onRefresh();web的IOC容器重寫了onRefresh方法。
  2. Web IOC容器創建嵌入式的Servlet容器。
private void createWebServer() {
        WebServer webServer = this.webServer;
        ServletContext servletContext = this.getServletContext();
        if (webServer == null && servletContext == null) {
            ServletWebServerFactory factory = this.getWebServerFactory();
            this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
        } else if (servletContext != null) {
            try {
                this.getSelfInitializer().onStartup(servletContext);
            } catch (ServletException var4) {
                throw new ApplicationContextException("Cannot initialize servlet context", var4);
            }
        }

        this.initPropertySources();
    }
  1. 獲取嵌入式的Servlet容器工廠: ServletWebServerFactory factory = this.getWebServerFactory();從IOC容器中獲取ServletWebServerFactory組件;

  2. 使用容器工廠獲取嵌入式的Servlet容器:his.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});

public WebServer getWebServer(ServletContextInitializer... initializers) {
        Tomcat tomcat = new Tomcat();
        File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        Connector connector = new Connector(this.protocol);
        tomcat.getService().addConnector(connector);
        this.customizeConnector(connector);
        tomcat.setConnector(connector);
        tomcat.getHost().setAutoDeploy(false);
        this.configureEngine(tomcat.getEngine());
        Iterator var5 = this.additionalTomcatConnectors.iterator();

        while(var5.hasNext()) {
            Connector additionalConnector = (Connector)var5.next();
            tomcat.getService().addConnector(additionalConnector);
        }

        this.prepareContext(tomcat.getHost(), initializers);
        return this.getTomcatWebServer(tomcat);
    }
  1. 嵌入式的Servlet容器創建並啟動Servlet容器。

9.先啟動嵌入式的Servlet容器,再將IOC容器中剩余的沒有創建出來的對象獲取出來、IOC容器啟動就會創建嵌入式的Servlet容器。

7.使用外置的Servlet容器

1. 嵌入式Servlet容器優缺點

優點:簡單、便捷。
缺點:默認不支持JSP,優化定制比較復雜。

2.使用外部Servlet容器步驟

  1. 必須創建一個war項目。
  2. 將嵌入式的Tomcat指定為provided。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring‐boot‐starter‐tomcat</artifactId>
    <scope>provided</scope>
</dependency>

  1. 必須編寫一個SpringBootServletInitializer的子類,並調用configure方法。
public class ServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        //傳入SpringBoot應用的主程序
        return application.sources(SpringBoot04WebJspApplication.class);
  }
}

  1. 啟動服務器就可以了。

3.原理與規則

原理
啟動服務器,服務器啟動SpringBoot應用[SpringBootServletInitializer],啟動IOC容器。

規則

  1. 服務器啟動會創建當前web應用里面每一個jar包里面的ServletContainerInitializer實例。
  2. ServletContainerInitializer的實現放在jar包的META-INF/services文件下,有一個名為javax.servlet.ServletContainerInitializer的文件,內容就是ServletContainerInitializer實現類的全類名。
  3. 還可以使用@HandlerType,在啟動應用時加載指定的類。

4. 啟動流程

  1. 啟動tomcat。
  2. 加載spring-web包下META-INF/services下面的javax.servlet.ServletContainerInitializer文件。

  1. SpringServletContainerInitializer將@HandlerType標注的所有這個類型的類都傳入搭配onStartup方法的Set中,為這些WebApplicationInitializer類型的類創建實例。
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    public SpringServletContainerInitializer() {
    }

    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        List<WebApplicationInitializer> initializers = new LinkedList();
        Iterator var4;
        if (webAppInitializerClasses != null) {
            var4 = webAppInitializerClasses.iterator();

            while(var4.hasNext()) {
                Class<?> waiClass = (Class)var4.next();
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
                    } catch (Throwable var7) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
        } else {
            servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
            AnnotationAwareOrderComparator.sort(initializers);
            var4 = initializers.iterator();

            while(var4.hasNext()) {
                WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
                initializer.onStartup(servletContext);
            }

        }
    }
}
  1. 每個WebApplicationInitializer到調用自己的onStartup()方法。

image.png

  1. 相當於SpringBootServletInitializer的類會被創建對象,並執行onStartup()方法。

  2. SpringBootServletInitializer實例執行onStartup的時候會crateRootApplicationContext創建容器。

protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
        // 1.創建SpringApplicationBuilder
        SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
        builder.main(this.getClass());
        ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext);
        if (parent != null) {
            this.logger.info("Root context already created (using as parent).");
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, (Object)null);
            builder.initializers(new ApplicationContextInitializer[]{new ParentContextApplicationContextInitializer(parent)});
        }

        builder.initializers(new ApplicationContextInitializer[]{new ServletContextApplicationContextInitializer(servletContext)});
        builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
        // 2.調用configure方法,子類重新了這個方法,將SpringBoot的主程序類傳入
        builder = this.configure(builder);
        builder.listeners(new ApplicationListener[]{new SpringBootServletInitializer.WebEnvironmentPropertySourceInitializer(servletContext)});
        // 3.使用builder創建一個Spring應用
        SpringApplication application = builder.build();
        if (application.getAllSources().isEmpty() && AnnotationUtils.findAnnotation(this.getClass(), Configuration.class) != null) {
            application.addPrimarySources(Collections.singleton(this.getClass()));
        }

        Assert.state(!application.getAllSources().isEmpty(), "No SpringApplication sources have been defined. Either override the configure method or add an @Configuration annotation");
        //確保錯誤頁被注冊
        if (this.registerErrorPageFilter) {
            application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
        }
        // 4. q啟動Spring應用
        return this.run(application);
    }
  1. Spring的應用啟動並且創建IOC容器。

  2. 先啟動Servlet容器,再啟動SpringBoot應用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM