1. 程式人生 > >SpringBoot之Web開發後續處理

SpringBoot之Web開發後續處理

回顧:

SpringBoot之基礎

SpringBoot之配置

SpringBoot之日誌

SpringBoot之Web開發基礎

SpringBoot之Web開發實驗

錯誤處理機制

1. SpringBoot預設的錯誤處理機制

    ① pc端訪問

瀏覽器傳送請求的請求頭:

    ② 客戶端訪問(預設響應一個json格式的資料)

客戶端傳送請求的請求頭:

    原理: 參照錯誤處理的自動配置類(ErrorMvcAutoConfiguration)

    ErrorMvcAutoConfiguration給容器中添加了以下元件:

        1) DefaultErrorAttributes

        頁面共享資訊:

        2) BasicErrorController(處理預設的/error請求)

         3) ErrorPageCustomizer(定製錯誤的響應規則)

            系統出現錯誤以後來到error請求進行處理

        4) DefaultErrorViewResolver

public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    ModelAndView modelAndView = this.resolve(String.valueOf(status), 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) {

//預設SpringBoot可以去找到一個頁面  如: error/404
    String errorViewName = "error/" + viewName;

//如果模板引擎可以解析這個頁面地址就用模板引擎解析, 返回一個ModelAndView
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);

//如果模板引擎不可解析, 則在靜態資原始檔夾下找errorViewName對應的頁面  如error/404.html
    return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}

        ① 一旦系統出現4xx或5xx之類的錯誤, ErrorPageCustomizer就會生效.

        ② 此時就會被BasicErrorController處理, 根據請求頭做區分, 判斷是返回html頁面的資料還是json格式的資料.

@RequestMapping(
    produces = {"text/html"}    //將會產生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 ? new ModelAndView("error", model) : modelAndView;
}

@RequestMapping
@ResponseBody    //產生json格式資料, 其他客戶端傳送的請求來到這個方法處理
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);
}

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
    Iterator var5 = this.errorViewResolvers.iterator();

    ModelAndView modelAndView;
    do {
        if (!var5.hasNext()) {
            return null;
        }

//根據所有的ErrorViewResolver(DefaultErrorViewResolver的子類)得到ModelAndView

        ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
        modelAndView = resolver.resolveErrorView(request, status, model);
    } while(modelAndView == null);

    return modelAndView;
}

2. 自定義響應

    ① 定製錯誤的頁面(pc端)

        1) 有模板引擎的情況下, SpringBoot是在模板資料夾下找對應狀態碼的檔案, 如error/404.html, 即只需在templates資料夾下新建error/404.html, 即可在返回狀態碼是404的情況下跳轉到該自定義的頁面.

也可以通過4xx.html非精確匹配, 當精確匹配不到具體的頁面時, 則如果是4開頭的狀態碼, 則匹配至4xx.html頁面

        頁面能獲取的資訊:

        ● timestamp: 時間戳

        ● status: 狀態碼

        ● error: 錯誤提示

        ● exception: 異常物件

        ● message: 異常的訊息

        ● errors: JSR303資料校驗的錯誤資訊

        2) 在模板引擎無法解析的情況下, 則在靜態資原始檔夾下找

        3) 以上都不滿足的情況下, 則跳轉SpringBoot預設的錯誤頁面.

    ② 定製json格式資料(客戶端)

    自定義異常類:

        1) 自定義異常處理, 返回定製的json資料

        2) 轉發到/error請求進行自適應響應效果處理

        3) 將定製的資料攜帶出去(以上兩種方式是無法攜帶定製的資料的) 

        出現錯誤以後, 會轉發至/error請求, 被BasicErrorController處理, 響應出去可以獲取的資料是由getErrorAttributes得到的, 該方法是父類(AbstractErrorController(ErrorController))規定的方法, SpringBoot規定, 當容器中沒有ErrorController的bean時, 則使用BasicErrorController, 那麼解決辦法

        ● 自定義一個ErrorController的實現類(或者是編寫AbstractErrorController的子類), 放入容器中即可.

        ● 頁面上能用的資料或者json返回的資料都是通過errorAttributes.getErrorAttributes得到的, 是由容器中                                             DefaultErrorAttributes.getErrorAttributes()預設進行資料處理的;

        最終的效果: 響應是自適應的, 可以通過ErrorAttributes改變需要返回的內容.

        頁面:

        json資料:

配置嵌入式Servlet容器

SpringBoot預設使用的是嵌入式的Servlet容器(Tomcat)

1) 配置內嵌的Servlet容器(Tomcat)

    ① 在配置檔案中直接修改和server有關的配置(application.properties / application.yml)

        server.port=8081
        server.context-path=/crud
        server.tomcat.uri-encoding=UTF-8

    ② EmbeddedServletContainerCustomizer(嵌入式的Servlet容器的定製器, 用來修改Servlet容器的配置)

    //配置嵌入式的Servlet容器
    @Bean
    public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
        return new EmbeddedServletContainerCustomizer() {

            //定製嵌入式的Servlet容器相關的規則
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                container.setPort(8083);    //修改容器的埠
            }
        };
    }

2) 註冊三大元件(Servlet / Filter / Listener)

    利用ServletRegistrationBean / FilterRegistrationBean / ServletListenerRegistrationBean對三大元件進行註冊.

    ① ServletRegistrationBean(註冊Servlet)

    public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req,resp);
    }

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

    @Bean
    public ServletRegistrationBean myServlet(){
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(),"/myServlet");
        registrationBean.setLoadOnStartup(1);
        return registrationBean;
    }

    ② FilterRegistrationBean(註冊Filter)

    public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("MyFilter process...");
        chain.doFilter(request,response);

    }

    @Override
    public void destroy() {

    }
}

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

    ③ ServletListenerRegistrationBean(註冊Listener)

    public class MyListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("contextInitialized...web應用啟動");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("contextDestroyed...當前web專案銷燬");
    }
}

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

3) 切換其他的Servlet容器

    SpringBoot預設支援在三種容器之間切換

    ① Tomcat(預設使用)    

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

    ② Jetty(適合開發長連線應用)
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>

    ③ Undertow(不支援jsp, 但併發效能好)

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

4) 嵌入式Servlet容器的自動配置原理

    ① EmbeddedServletContainerAutoConfiguration(嵌入式的Servlet容器自動配置)

        @AutoConfigureOrder(-2147483648)
        @Configuration
        @ConditionalOnWebApplication
        @Import({EmbeddedServletContainerAutoConfiguration.BeanPostProcessorsRegistrar.class})
        public class EmbeddedServletContainerAutoConfiguration {

            @Configuration
            @ConditionalOnClass({Servlet.class, Tomcat.class})   //兩個類: 原生Servlet和Tomcat, 判斷是否引入了Tomcat的依賴
            @ConditionalOnMissingBean(
            value = {EmbeddedServletContainerFactory.class},    //判斷當前容器中有沒有使用者自己定義的嵌入式的Servlet容器工廠
            //該工廠的作用是建立嵌入式的Servlet容器

            search = SearchStrategy.CURRENT
            )
            public static class EmbeddedTomcat {
            public EmbeddedTomcat() {
            }

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

     }

        EmbeddedServletContainerFactory(獲取嵌入式的Servlet容器 ==> EmbeddedServletContainer)

        該工廠的繼承樹正式三個內嵌的Servlet容器(Tomcat / Jetty / Undertow)

        EmbeddedServletContainer(容器)

        以TomcatEmbeddedServletContainerFactory為例

        public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers) {
            Tomcat tomcat = new Tomcat();    //建立一個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);
            return this.getTomcatEmbeddedServletContainer(tomcat);   //將配置好的Tomcat傳入進去, 返回一個嵌入式的Tomcat容器
        }    //該方法執行完成之後則啟動Tomcat容器

步驟小結:

① SpringBoot根據匯入的依賴情況, 給容器中新增相應的

② 容器中某個元件要建立物件就會驚動後置處理器, 只要是嵌入式的Servlet容器工廠, 後置處理器就工作.

③ 後置處理器, 從容器中獲取所有的定製器, 呼叫定製器的定製方法.

5) 嵌入式Servlet容器的啟動原理

    步驟:

    ① SpringBoot應用啟動執行run方法

    ② 建立IOC容器物件,並初始化容器,建立容器中的每一個元件, 如果是web應用, 則建立                                                                   AnnotationConfigEmbeddedWebApplicationContext, 如果不是, 則建立AnnotationConfigApplicationContext

    ③ 重新整理建立好的ioc容器

    ④ webIOC容器會建立嵌入式的Servlet容器

    ⑤ 獲取嵌入式的Servlet容器工廠

        EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();

        從ioc容器中獲取EmbeddedServletContainerFactory 元件, TomcatEmbeddedServletContainerFactory建立物件,後置                  處理器一看是這個物件,就獲取所有的定製器來先定製Servlet容器的相關配置;

    ⑥ 使用容器工廠獲取嵌入式的Servlet容器: this.embeddedServletContainer = containerFactory                                                        .getEmbeddedServletContainer(getSelfInitializer());

    ⑦ 嵌入式的Servlet容器建立物件並啟動Servlet容器,先啟動嵌入式的Servlet容器,再將ioc容器中剩下沒有創建出的物件獲取出來, 也就是IOC容器啟動並建立嵌入式的Servlet容器

配置外接的Servlet容器

優點: 簡單 / 便捷

缺點: 不支援jsp / 優化和定製比較複雜

    1) 安裝外部Tomcat環境並配置web專案

   

    2) 小結

        ① 必須建立一個war專案

        ② 將嵌入式的Tomcat指定為provided

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

        ③ 必須編寫一個SpringBootInitializer的實現類, 並重寫configure方法

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {    //SpringBoot應用程式的構建器
        return application.sources(SpringBoot04WebJspApplication.class);    //傳入SpringBoot應用的主程式
    }

}

        ④ 啟動伺服器即可使用外部Tomcat

外部Tomcat啟動SpringBoot的原理

區別:

jar包: 執行SpringBoot的主方法, 啟動ioc容器, 建立嵌入式的Servlet容器

war包: 啟動伺服器, 伺服器啟動SpringBoot應用, 再啟動ioc容器

servlet3.0(Spring註解版):

8.2.4 Shared libraries / runtimes pluggability:

規則:

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

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

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

流程:

① 啟動Tomcat

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

Spring的web模組裡面有這個檔案:org.springframework.web.SpringServletContainerInitializer

③ SpringServletContainerInitializer將@HandlesTypes(WebApplicationInitializer.class)標註的所有這個型別的類都傳入到onStartup方法的Set<Class<?>>;為這些WebApplicationInitializer型別的類建立例項;

④ 每一個WebApplicationInitializer都呼叫自己的onStartup;

⑤ 相當於我們的SpringBootServletInitializer的類會被建立物件,並執行onStartup方法

⑥ SpringBootServletInitializer例項執行onStartup的時候會createRootApplicationContext;建立容器

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));
   builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
    
    //呼叫configure方法,子類重寫了這個方法,將SpringBoot的主程式類傳入了進來
   builder = configure(builder);
    
    //使用builder建立一個Spring應用
   SpringApplication application = builder.build();
   if (application.getSources().isEmpty() && AnnotationUtils
         .findAnnotation(getClass(), Configuration.class) != null) {
      application.getSources().add(getClass());
   }
   Assert.state(!application.getSources().isEmpty(),
         "No SpringApplication sources have been defined. Either override the "
               + "configure method or add an @Configuration annotation");

   if (this.registerErrorPageFilter) {
      application.getSources().add(ErrorPageFilterConfiguration.class);
   }
    //啟動Spring應用
   return run(application);
}

7)、Spring的應用就啟動並且建立IOC容器

public ConfigurableApplicationContext run(String... args) {
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   ConfigurableApplicationContext context = null;
   FailureAnalyzers analyzers = null;
   configureHeadlessProperty();
   SpringApplicationRunListeners listeners = getRunListeners(args);
   listeners.starting();
   try {
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(
            args);
      ConfigurableEnvironment environment = prepareEnvironment(listeners,
            applicationArguments);
      Banner printedBanner = printBanner(environment);
      context = createApplicationContext();
      analyzers = new FailureAnalyzers(context);
      prepareContext(context, environment, listeners, applicationArguments,
            printedBanner);
       
       //重新整理IOC容器
      refreshContext(context);
      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);
   }
}