1. 程式人生 > >SpringBoot配置嵌入式Servlet容器

SpringBoot配置嵌入式Servlet容器

一、如何定製和修改Servlet容器的相關配置


 前言:SpringBoot在Web環境下,預設使用的是Tomact作為嵌入式的Servlet容器;
 
1)、修改和server相關的配置(ServerProperties實現了EmbeddedServletContainerCustomizer)例如:修改埠號

#通用的Servlet容器設定:修改埠號
server:
  port: 8081
  tomcat:  #設定Tomact的相關屬性,例如編碼格式
    uri-encoding: utf-8

  ☞ 我們也可以進入port所屬的物件中,發現其他可修改的引數等等,如下:

@ConfigurationProperties(
    prefix = "server",
    ignoreUnknownFields = true
)
public class ServerProperties implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered {
    private Integer port;
    private InetAddress address;
    private String contextPath;
    private String displayName = "application";
    ......

2)、編寫一個EmbeddedServletContainerCustomizer:嵌入式的Servlet容器的定製器,來修改Servlet容器的配置。其實1中的ServerProperties也是實現了EmbeddedServletContainerCustomizer。xxxCustomizer幫組我們進行定製配置。

@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
    @Bean
    public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                container.setPort(8082);
            }
        };
    }

二、註冊Servlet三大元件【Servlet、Filter、Listener】


  由於SpringBoot預設是以jar包的方式啟動嵌入的Servlet容器來啟動SpringBoot的web應用,沒有web.xml檔案。
  註冊三大元件的方式如下:1)、通過ServletRegistrationBean註冊自定義的Servlet。

//首先建立一個Servlet
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("Hello MyServlet");
        super.doPost(req, resp);
    }
}

//將建立的Servlet通過配置類注入到容器中,兩個是不同的類。
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
    @Bean
    public ServletRegistrationBean myServlet(){
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(), "/myServlet");
        return registrationBean;
    }

2)、通過FilterRegistrationBean註冊攔截器Filter。

//自定義一個filter實現servlet.Filter介面
public class myFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.printf("myFilter");
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {

    }
}

//通過配置類注入自定義的Filter
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
    @Bean
    public FilterRegistrationBean myFilter(){
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new myFilter());
        registrationBean.setUrlPatterns(Arrays.asList("/hello","/myFilter"));
        return registrationBean;
    }

3)、通過ServletListenerRegistrationBean註冊自定義的Listener。

//建立自定義的Listener監聽
public class myListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.printf("服務啟動");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.printf("服務銷燬");
    }
}

//通過配置類注入自定義的listener
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {

    public ServletListenerRegistrationBean myListener(){
        ServletListenerRegistrationBean<MyListener> servletListenerRegistrationBean = new ServletListenerRegistrationBean<>(new MyListener());
        return servletListenerRegistrationBean;
    }

三、使用其他Servlet容器:Jetty(長連線引用)、Undertow(不支援JSP)


    1)、我們在定製嵌入式的Servlet容器時,會傳入ConfigurableEmbeddedServletContainer類,我們通過Ctrl+T檢視此可配置嵌入式類容器中可以配置Tomcat、Jetty和Undertow。

    //ConfigurableEmbeddedServletContainer 
    @Bean
    public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                container.setPort(8082);
            }
        };
    }

 
    2)、預設使用Tomcat,因為starter-web引入的是Tomcat的starter。我們排除Tomcat的依賴,引入Jetty的依賴即可。

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

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

四、嵌入式Servlet容器自動配置原理


1)、EmbeddedServletContainerAutoConfiguration類主要用來自動配置嵌入式的Servlet容器。

@AutoConfigureOrder(-2147483648)
@Configuration
@ConditionalOnWebApplication
 //匯入BeanPostProcessorsRegistrar:後置處理器:在bean初始化前後,執行(剛建立完物件,還沒屬性賦值)初始化工作.
 //給容器中匯入一些元件,匯入了embeddedServletContainerCustomizerBeanPostProcessor
@Import({EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar.class})
public class EmbeddedServletContainerAutoConfiguration {
    @Configuration
    @ConditionalOnClass({Servlet.class, Tomcat.class})//判斷當前Servlet中是否引入的Tomcat依賴
    @ConditionalOnMissingBean(
        value = {EmbeddedServletContainerFactory.class},
        search = SearchStrategy.CURRENT
    )//判斷當前容器中,沒有使用者自定義的EmbeddedServletContainerFactory嵌入式的Servlet容器工廠,
    //作用:建立嵌入式的servlet容器。
    public static class EmbeddedTomcat {
        public EmbeddedTomcat() {
        }

        @Bean
        public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
            return new TomcatEmbeddedServletContainerFactory();
        }
    }

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

        @Bean
        public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
            return new UndertowEmbeddedServletContainerFactory();
        }
    }

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

        @Bean
        public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
            return new JettyEmbeddedServletContainerFactory();
        }
    }
}

2)嵌入式的容器工廠:EmbeddedServletContainerFactory ,用來建立嵌入式的Servlet容器。

public interface EmbeddedServletContainerFactory {
     //獲取嵌入式的Servlet容器
     EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... var1); 
}

  ☛ SpringBoot再帶了三種嵌入式的容器工廠,如下:

3)、EmbeddedServletContainer:嵌入式的容器,SpringBoot為我們提供了三種不同的嵌入式容器,與工廠相互對應,如下:
 

4)、我們進入工廠類TomcatEmbeddedServletContainerFactory發現,其實也是建立一個Tomcat並配置其基本屬性。

    public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
        //建立Tomcat
        Tomcat tomcat = new Tomcat();

        //配置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);
        //將配置好的Tomcat傳入,並啟動Tomcat,Tomcat.start()
        return this.getTomcatEmbeddedServletContainer(tomcat);
    }

5)、使用者自定義的Servlet容器配置類和SpringBoot預設的ServerProperties配置類,都實現了EmbeddedServletContainerCustomizer介面。到底是怎麼實現的哪?其實是SpringBoot自動配置類中引入了後置處理器,如下:

//與使用者自定義的Servlet容器實現的介面名很類似,有一定的命名規則。
embeddedServletContainerCustomizerBeanPostProcessor

  ☛ 進入後置處理器類中,重點看如下程式碼:

    //初始化之前執行
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        //如果當前初始化的是當前ConfigurableEmbeddedServletContainer型別的元件
        if(bean instanceof ConfigurableEmbeddedServletContainer) {
            this.postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer)bean);
        }

        return bean;
    }

    //上面的postProcessBeforeInitialization方法:
    private void postProcessBeforeInitialization(ConfigurableEmbeddedServletContainer bean) {
        Iterator var2 = this.getCustomizers().iterator();

        while(var2.hasNext()) {
            //獲取所有的定製器,呼叫每一個定製器的customize方法來給servlet屬性賦值。
            EmbeddedServletContainerCustomizer customizer = (EmbeddedServletContainerCustomizer)var2.next();
            customizer.customize(bean);
        }

    private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
        if(this.customizers == null) {
            //this.beanFactory.xx表示從容器中獲取XXCustomizer自定義型別的元件
            this.customizers = new ArrayList(this.beanFactory.getBeansOfType(EmbeddedServletContainerCustomizer.class, false, false).values());
            Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
            this.customizers = Collections.unmodifiableList(this.customizers);
        }

        return this.customizers;
    }
    }

整理下步驟:【1】、SpringBoot根據pom.xml中匯入的依賴,給容器中新增其對應的嵌入式的服務容器工廠類,例如預設的Tomcat工廠:EmbeddedServletContainerFactory【TomcatEmbeddedServletContainerFactory】
【2】、給容器中某個元件要建立物件就會觸發後置處理器EmbeddedServletContainerCustomizerBeanPostProcessor,只要是嵌入式的Servlet容器工廠,後置處理器就會工作(預設的ServerProperties也是實現了此類介面的,所以肯定存在相關配置類)
【3】、後置處理器從容器中獲取所有的EmbeddedServletContainerCustomizer,呼叫定製器的定製方法。

五、嵌入式Servlet容器啟動原理


根據上述的流程,我們要研究Servlet容器的啟動原理。其實就是研究什麼時候建立嵌入式的容器工廠和何時獲取嵌入式的容器並啟動Tomcat。
獲取嵌入式的Servlet容器工廠的過程(在new TomcatEmbeddedServletContainerFactory()時打一個斷電,檢視過程):
  ● SpringBoot應用啟動執行run()方法。
  ● this.refreshContext(context)方法:用來初始化IOC容器,既建立IOC容器物件並初始化IOC容器中的每一個元件。

    protected ConfigurableApplicationContext createApplicationContext() {
        Class<?> contextClass = this.applicationContextClass;
        if(contextClass == null) {
            try {
                //判斷是不是web環境,是Web環境引入AnnotationConfigEmbeddedWebApplicationContext,否則引入AnnotationConfigApplicationContext
                contextClass = Class.forName(this.webEnvironment
                ?"org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext"
                :"org.springframework.context.annotation.AnnotationConfigApplicationContext");
            } catch (ClassNotFoundException var3) {
                throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
            }
        }

        return (ConfigurableApplicationContext)BeanUtils.instantiate(contextClass);
    }

  ● this.refresh(context):重新整理剛才建立好的IOC容器。
  ● this.onRefresh():web的Ioc容器重寫了onRefresh()方法。

    protected void onRefresh() {
        super.onRefresh();

        try {
            //重點是建立了嵌入式的Servlet容器
            this.createEmbeddedServletContainer();
        } catch (Throwable var2) {
            throw new ApplicationContextException("Unable to start embedded container", var2);
        }
    }

  ● this.createEmbeddedServletContainer():web的IOC容器會建立嵌入式的Servlet容器。

    private void createEmbeddedServletContainer() {
        EmbeddedServletContainer localContainer = this.embeddedServletContainer;
        ServletContext localServletContext = this.getServletContext();
        if(localContainer == null && localServletContext == null) {
            // 1、獲取嵌入式的Servlet嵌入式的工廠
            EmbeddedServletContainerFactory containerFactory = this.getEmbeddedServletContainerFactory();
            this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(
                    new ServletContextInitializer[]{this.getSelfInitializer()});
        } 
    }

  ● 獲取嵌入式工廠後,便可從容器中獲取EmbeddedServletContainerFactory的元件tomcatEmbeddedServletContainerFactory來建立Tomcat物件,後置處理器就會觸發獲取所有的定製器來確定Servlet容器的相關配置。
  ● 通過嵌入式工廠獲取嵌入式容器,如下:

            this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(
                    new ServletContextInitializer[]{this.getSelfInitializer()});

  ● 嵌入式的Servlet容器建立並啟動物件:

    public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
        //建立物件
        Tomcat tomcat = new Tomcat();

        //啟動物件
        this.tomcat.start();

  ● 先啟動嵌入式的Servlet容器,再將IOC容器中剩下沒有建立的物件進行初始化,如下:

                this.onRefresh();
                //啟動完嵌入式容器後,後續還有其他物件的初始化工作
                this.registerListeners();
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var9) {

六、使用外接的Servlet容器


嵌入式Servlet容器的缺點:預設不支援JSP、優化和定製比較複雜。
外接Servlet容器:安裝外部的Tomcat,步驟如下:
1)、必須建立一個war專案,需要手動建立目錄(利用Idea快速建立)如下:

2)、將嵌入式的Tomcat指定為provide(Idea建立完後,會自動幫我們完成,但我們需要了解)

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-tomcat</artifactId>
		<scope>provided</scope>
	</dependency>

3)、需要編寫一個SpringBootServletInitializer的子類,並呼叫configure方法:

public class ServletInitializer extends SpringBootServletInitializer {

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
		return application.sources(SpringBootWebApplication.class);
	}
}

4)、配置本地的Tomcat,並啟動Tomcat即可。(此專案執行run()方法是不能啟動專案的):需要設定名稱和本地Tomcat的路徑即可使用外部Servlet。   

七、外接伺服器的使用原理


 ☞  jar包:執行SpringBoot主類的main方法,啟動並初始化IOC容器且建立嵌入式的Servlet容器。
 ☞  war包:啟動伺服器後呼叫SpringBootServletInitializer中的configure()方法,載入我們的SpringBoot應用並啟動。
Servlet3.0規則:1)、伺服器啟動後,會建立當前web應用中包含的每個jar內的ServletContainerInitializer例項。
   2)、ServletContainerInitializer的實現放在jar包的META-INF/services資料夾下(javax.servlet.ServletContainerInitializer:內容就是ServletContainerInitializer的全類名)
  3)、可以使用@handlesTypes註解,在應用啟動時載入我們需要的類。
流程:1)、啟動Tomcat後,獲取servlet.ServletContainerInitializer檔案如下:其中的內容同下:

#檔案中的內容
org.springframework.web.SpringServletContainerInitializer

  2)、進入SpringServletContainerInitializer發現此類將@HandlesTypes({WebApplicationInitializer.class})標註的所有這個型別的類都傳入到onStartup方法中的Set<Class<?>>,併為這些類建立例項。

@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    //onStartup方法,用來例項化感興趣的物件
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        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)waiClass.newInstance());
                    } catch (Throwable var7) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
                    }
                }
            }
        }

  3)、每一個WebApplicationInitializer都呼叫自己的onStartup()方法。

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

4)、WebApplicationInitializer只是一個藉口,其實現類主要有以下三個:SpringBootServletInitalizer正是SpringBoot給我們建立好的啟動類,會被建立物件,並啟動自身的onStartup()方法。

5)、執行onStartup()方法時,會呼叫createRootApplicationContext()方法來建立容器

public void onStartup(ServletContext servletContext) throws ServletException {
        this.logger = LogFactory.getLog(this.getClass());
        //建立容器
        WebApplicationContext rootAppContext = this.createRootApplicationContext(servletContext);
        if(rootAppContext != null) {
            servletContext.addListener(new ContextLoaderListener(rootAppContext) {
                public void contextInitialized(ServletContextEvent event) {
                }
            });

    //容器的具體呼叫實現
    protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
        //建立Spring應用的構建器
        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);

        //重要:子類中重寫了此方法,子類出入了應用的主程式類
        builder = this.configure(builder);
        builder.listeners(new ApplicationListener[]{new SpringBootServletInitializer.WebEnvironmentPropertySourceInitializer(servletContext, null)});
        //使用build()建立一個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));
        }
        //啟動應用
        return this.run(application);
    }

6)、執行應用的run()方法,來啟動Spring應用並建立IOC容器。

    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();

        Collection exceptionReporters;
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            Banner printedBanner = this.printBanner(environment);
            context = this.createApplicationContext();
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, new Object[]{context});
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);

            //重新整理IOC容器
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if(this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }

            listeners.started(context);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }

        try {
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

核心:先來啟動Servlet容器後啟動SpringBoot應用      over。。。。