1. 程式人生 > >專案打War包和外部Tomcat配置與專案啟動原理

專案打War包和外部Tomcat配置與專案啟動原理

SpringBoot應用預設以Jar包方式並且使用內建Servlet容器(預設Tomcat),該種方式雖然簡單但是預設不支援JSP並且優化容器比較複雜。故而我們可以使用習慣的外接Tomcat方式並將專案打War包。

【1】建立專案並打War包

① 同樣使用Spring Initializer方式建立專案

這裡寫圖片描述

② 打包方式選擇”war”

這裡寫圖片描述

③ 選擇新增的模組

這裡寫圖片描述

④ 建立的專案圖示

這裡寫圖片描述

有三個地方需要注意:

  • pom中打包方式已經為war;
  • 對比預設為jar的專案多了ServletInitializer類;
  • 專案結構沒有src/main/webapp,且沒有WEB/INF web.xml。

ServletInitializer類如下:

public class ServletInitializer extends SpringBootServletInitializer {

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

}

pom檔案如下:

<?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.web</groupId> <artifactId>springbootwebproject</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>springbootwebproject</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--這裡修改了內建Tomcat的作用域--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

⑤ 補全專案結構

第一種方式,手動建立src/main/webapp, WEB/INF以及web.xml。

第二種方式,使用idea建立,步驟如下:

第一步:如下圖所示,點選專案結構圖示

這裡寫圖片描述

第二步:建立src/main/webapp

這裡寫圖片描述

第三步:建立web.xml

這裡寫圖片描述
這裡寫圖片描述

此時專案結構圖如下:

這裡寫圖片描述

【2】使用外部配置的Tomcat啟動專案

① 點選”Edit Configurations…”新增Tomcat。

這裡寫圖片描述

② 設定Tomcat、JDK和埠

這裡寫圖片描述

③ 部署專案

這裡寫圖片描述
這裡寫圖片描述

④ 啟動專案

這裡寫圖片描述

此時如果webapp 下有index.html,index.jsp,則會預設訪問index.html。

如果只有index.jsp,則會訪問index.jsp;如果webapp下無index.html或index.jsp,則從靜態資原始檔夾尋找index.html;如果靜態資原始檔夾下找不到index.html且專案沒有對”/”進行額外攔截處理,則將會返回預設錯誤頁面。

index.html顯示如下圖:

這裡寫圖片描述

【3】外部Tomcat啟動原理

① 首先看Servlet3.0中的規範

The ServletContainerInitializer class is looked up via the jar services API.
For each application, an instance of the ServletContainerInitializer is
created by the container at application startup time. The framework providing an
implementation of the ServletContainerInitializer MUST bundle in the
META-INF/services directory of the jar file a file called
javax.servlet.ServletContainerInitializer, as per the jar services API,
that points to the implementation class of the ServletContainerInitializer.

In addition to the ServletContainerInitializer we also have an annotation -
HandlesTypes. The HandlesTypes annotation on the implementation of the
ServletContainerInitializer is used to express interest in classes that may
have annotations (type, method or field level annotations) specified in the value of
the HandlesTypes or if it extends / implements one those classes anywhere in the
class’ super types. The HandlesTypes annotation is applied irrespective of the
setting of metadata-complete.

The onStartup method of the ServletContainerInitializer will be invoked
when the application is coming up before any of the servlet listener events are fired.

The onStartup method of the ServletContainerInitializer is called with a
Set of Classes that either extend / implement the classes that the initializer
expressed interest in or if it is annotated with any of the classes specified via the
@HandlesTypes annotation.

總結以下幾點:

1)伺服器啟動(web應用啟動)會建立當前web應用裡面每一個jar包裡面ServletContainerInitializer例項;

2)jar包的META-INF/services資料夾下,有一個名為javax.servlet.ServletContainerInitializer的檔案,內容就是ServletContainerInitializer的實現類的全類名;

如下圖所示:
這裡寫圖片描述

3)還可以使用@HandlesTypes,在應用啟動的時候載入我們感興趣的類;

4)容器啟動過程中首先呼叫ServletContainerInitializer 例項的onStartup方法。

② 步驟分析如下

第一步,Tomcat啟動

這裡寫圖片描述

第二步,根據Servlet3.0規範,找到ServletContainerInitializer ,進行例項化

jar包路徑:

org\springframework\spring-web\4.3.14.RELEASE\
spring-web-4.3.14.RELEASE.jar!\METAINF\services\
javax.servlet.ServletContainerInitializer:

Spring的web模組裡面有這個檔案:

org.springframework.web.SpringServletContainerInitializer

這裡寫圖片描述

第三步,建立例項

SpringServletContainerInitializer@HandlesTypes(WebApplicationInitializer.class)標註的所有這個型別
的類都傳入到onStartup方法的Set,為這些WebApplicationInitializer型別的類建立例項並遍歷呼叫其onStartup方法。

SpringServletContainerInitializer 原始碼如下(呼叫其onStartup方法):

//傳入感興趣的類
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    public SpringServletContainerInitializer() {
    }
    //呼叫onStartup方法
    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();
                //對感興趣的類進行判斷然後新增到initializers
                if(!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance(new Object[0]));
                    } 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方法
                initializer.onStartup(servletContext);
            }

        }
    }
}

如上所示,在SpringServletContainerInitializer方法中又呼叫每一個initializer的onStartup方法。

WebApplicationInitializer型別的類如下圖:

這裡寫圖片描述

可以看到,將會建立我們的ServletInitializer(繼承自SpringBootServletInitializer)例項。

第四步:我們的SpringBootServletInitializer的類會被建立物件,並執行onStartup方法

SpringBootServletInitializer原始碼如下:

@Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        // Logger initialization is deferred in case an ordered
        // LogServletContextInitializer is being used
        this.logger = LogFactory.getLog(getClass());
        //建立WebApplicationContext 
        WebApplicationContext rootAppContext = createRootApplicationContext(
                servletContext);
        if (rootAppContext != null) {
            servletContext.addListener(new ContextLoaderListener(rootAppContext) {
                @Override
                public void contextInitialized(ServletContextEvent event) {
                    // no-op because the application context is already initialized
                }
            });
        }
        else {
            this.logger.debug("No ContextLoaderListener registered, as "
                    + "createRootApplicationContext() did not "
                    + "return an application context");
        }
    }

建立WebApplicationContext 原始碼如下:

protected WebApplicationContext createRootApplicationContext(
            ServletContext servletContext) {
            //建立SpringApplicationBuilder
        SpringApplicationBuilder builder = createSpringApplicationBuilder();
        StandardServletEnvironment environment = new StandardServletEnvironment();
        environment.initPropertySources(servletContext, null);
        builder.environment(environment);
        builder.main(getClass());
        ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
        if (parent != null) {
            this.logger.info("Root context already created (using as parent).");
            servletContext.setAttribute(
                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
            builder.initializers(new ParentContextApplicationContextInitializer(parent));
        }
        builder.initializers(
                new ServletContextApplicationContextInitializer(servletContext));

 //呼叫configure方法,子類重寫了這個方法,將SpringBoot的主程式類傳入了進來
        builder = configure(builder);

        //使用builder建立一個Spring應用
        SpringApplication application = builder.build();
        if (application.getAllSources().isEmpty() && AnnotationUtils
                .findAnnotation(getClass(), Configuration.class) != null) {
            application.addPrimarySources(Collections.singleton(getClass()));
        }
        Assert.state(!application.getAllSources().isEmpty(),
                "No SpringApplication sources have been defined. Either override the "
                        + "configure method or add an @Configuration annotation");
        // Ensure error pages are registered
        if (this.registerErrorPageFilter) {
            application.addPrimarySources(
                    Collections.singleton(ErrorPageFilterConfiguration.class));
        }
        //啟動Spring應用
        return run(application);
    }

至此,應用被啟動,開始執行應用啟動的一系列方法。

/**
     * Run the Spring application, creating and refreshing a new
     * {@link ApplicationContext}.
     * @param args the application arguments (usually passed from a Java main method)
     * @return a running {@link ApplicationContext}
     */
    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                    args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners,
                    applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            exceptionReporters = getSpringFactoriesInstances(
                    SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
            prepareContext(context, environment, listeners, applicationArguments,
                    printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass)
                        .logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners);
            throw new IllegalStateException(ex);
        }

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