SpringBoot之Web开发后续处理

版权声明:原创文章, 欢迎转载. https://blog.csdn.net/ip_JL/article/details/84938123

回顾:

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);
   }
}

猜你喜欢

转载自blog.csdn.net/ip_JL/article/details/84938123