Loading

【SpringBoot1.x】SpringBoot1.x Web 开发

SpringBoot1.x Web 开发

文章源码

简介

SpringBoot 非常适合 Web 应用程序开发。可以使用嵌入式 Tomcat,Jetty 或 Undertow 轻松创建独立的 HTTP 服务器。

大多数Web应用程序将使用 spring-boot-starter-web 模块来快速启动和运行。

使用 SpringBoot 开发 Web 应用的流程:

  • 创建 SpringBoot 应用,选择需要集成的模块
  • SpringBoot 默认将这些模块场景配置好,只需要在配置文件指定相关属性即可
  • 编写相关的业务代码

xxxxAutoConfigurartion 自动配置类,给容器中添加组件。xxxxProperties 封装配置文件中相关属性。

SpringBoot 对静态资源的映射规则

对于静态资源文件夹映射

springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    Integer cachePeriod = this.resourceProperties.getCachePeriod();
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/").setCachePeriod(cachePeriod));
    }
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    // 静态资源文件夹映射
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                .addResourceLocations(this.resourceProperties.getStaticLocations())
                .setCachePeriod(cachePeriod));
    }
}

可以看到:

  • 所有以 /webjars/** 的方式访问项目的任何资源,都会去以下 classpath:/META-INF/resources/webjars/ 找资源。webjars 以 jar包 的方式引入静态资源,比如访问 localhost:8080/webjars/jquery/3.3.1/jquery.js
  • 所有以 /** 的方式访问当前项目的任何资源,都去静态资源的文件夹找资源。SpringBoot 默认的静态资源的文件夹有:
    • "classpath:/META-INF/resources/"
    • "classpath:/resources/"
    • "classpath:/static/"
    • "classpath:/public/"
    • 根目录

对于欢迎页映射

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ResourceProperties resourceProperties) {
    return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(),
            this.mvcProperties.getStaticPathPattern());
}

可以看到:

静态资源文件夹下的所有 index.html 页面,被 "/**" 映射,比如访问 localhost:8080/ 就会找 index 页面。


对于图标映射

@Configuration
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration {

    private final ResourceProperties resourceProperties;

    public FaviconConfiguration(ResourceProperties resourceProperties) {
        this.resourceProperties = resourceProperties;
    }

    @Bean
    public SimpleUrlHandlerMapping faviconHandlerMapping() {
        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
        // 所有的 **/favicon.ico
        mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", faviconRequestHandler()));
        return mapping;
    }

    @Bean
    public ResourceHttpRequestHandler faviconRequestHandler() {
        ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
        requestHandler.setLocations(this.resourceProperties.getFaviconLocations());
        return requestHandler;
    }

}

可以看到:

所有的 **/favicon.ico 都是在静态资源文件下找。


这些都是 SpringBoot 默认配置的属性,ResourceProperties 可用于配置资源处理的属性。

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties implements ResourceLoaderAware, InitializingBean {

    private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
            "classpath:/resources/", "classpath:/static/", "classpath:/public/" };

    private static final String[] RESOURCE_LOCATIONS;

    // ...

    // 静态资源的位置
    private String[] staticLocations = RESOURCE_LOCATIONS;

    // 资源处理程序所服务的资源的缓存周期(以秒为单位)
    private Integer cachePeriod;

    // ...
}

模版引擎

Template

...

Hello ${user}
...

Data

...

model.adddAttibute("user", "parzulpan")
...

通过 TemplateEngine 能得到:

...

Hello parzulpan
...

SpringBoot 推荐的模版引擎 有 Thymeleaf,它语法更简单,功能更强大。

引入 Thymeleaf

添加如下属性和依赖:

    <properties>
        <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
        <!-- 布局功能的支持程序  thymeleaf3主程序  layout2以上版本 -->
        <!-- thymeleaf2主程序   layout12以上版本 -->
        <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
    </properties>

    <!-- thymeleaf 模版引擎 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

Thymeleaf 使用

可以看到 ThymeleafProperties 为如下,可以根据它来配置使用:

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

    private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");

    private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");

    public static final String DEFAULT_PREFIX = "classpath:/templates/";

    public static final String DEFAULT_SUFFIX = ".html";
}

所以只要把 HTML 页面放在 classpath:/templates/,thymeleaf 就能自动渲染。

使用步骤

  • 导入 thymeleaf 的名称空间

    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
  • 使用 thymeleaf 语法

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF‐8">
        <title>Title</title>
    </head>
    <body>
        <h1>成功!</h1>
        <!-- th:text 将div里面的文本内容设置为 -->
        <div th:text="${hello}">这是显示欢迎信息</div>
    </body>
    </html>
    

语法规则

th:text,改变当前元素里面的文本内容。

th:xx ,xx 可以是任意 html 属性,用来替换原生属性的值。

SpringMVC 自动配置

SpringBoot 为 SpringMVC 提供了自动配置,可与大多数应用程序完美配合。自动配置在 Spring 的默认值之上添加了以下功能:

  • 包含 ContentNegotiatingViewResolver 和 BeanNameViewResolver
  • 支持提供静态资源,包括对 WebJars 的支持
  • 自动注册 Converter,GenericConverter,Formatter beans
  • 支持 HttpMessageConverters
  • 自动注册 MessageCodesResolver
  • 静态 index.html 支持。
  • 定制 Favicon 支持
  • 自动使用 ConfigurableWebBindingInitializer bean

如果您想保留 SpringBoot MVC功能,并且只想添加其他 MVC 配置(拦截器,格式化程序,视图控制器等),则可以添加自己 @Configuration 的配置类,继承自 WebMvcConfigurerAdapter,但不能添加 @EnableWebMvc。这样的话,既保留了所有的自动配置,也能用我们扩展的配置。

package cn.parzulpan.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 自动以配置类,扩展 SpringMVC
 */

@Configuration
public class CustomMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

        // 浏览器发送 /parzulpan 请求来到自定义 404 页面
        registry.addViewController("/parzulpan").setViewName("404");
    }
}

也可以全面接管 SpringMVC,使所有的 SpringMVC 的自动配置都失效,只使用自己的配置。只需要在配置类中添加 @EnableWebMvc 即可。

为什么会失效呢?

因为 @EnableWebMvc 会将 WebMvcConfigurationSupport 组件导入进来,而该组件导入进来后,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 {}

修改 SpringBoot 默认配置

SpringBoot 在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如果有就用用户配置的。如果没有,才自动配置。如果有些组件可以有多个(ViewResolver),可以将用户配置的和自己默
认的组合起来。

错误处理机制

配置嵌入式 Servlet 容器

SpringBoot 默认使用 Tomcat 作为嵌入式的 Servlet 容器。

定制和修改 Servlet 容器的相关配置

方式一:可以通过修改跟 server 有关的配置来定制和修改,能修改的配置可以参考 ServerProperties 类,这个类本质也是 EmbeddedServletContainerCustomizer

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

// 通用的 Servlet 容器设置
server.xxx
// Tomcat 的设置
server.tomcat.xxx

方式二:编写一个 EmbeddedServletContainerCustomizer,即嵌入式的 Servlet 容器的定制器,用它来修改 Servlet 容器的配置

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 自定义服务配置类
 */

@Configuration
public class CustomServerConfig {

    // 定制器 添加到容器中
    @Bean
    public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer(){
        return new EmbeddedServletContainerCustomizer() {
            // 定制嵌入式的Servlet容器相关的规则
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                container.setPort(8083);
            }
        };
    }
}

注册 Servlet 三大组件

三大组件即 Servlet、Filter、Listener。

由于 SpringBoot 默认是以 jar包 的方式启动嵌入式的 Servlet 容器来启动 SpringBoot 的 Web 应用,即没有 web.xml 文件。所以注册三个组件可以使用以下方式:

  • ServletRegistrationBean

  • FilterRegistrationBean

  • ServletListenerRegistrationBean

    /**
    * @Author : parzulpan
    * @Time : 2020-12
    * @Desc : 自定义 Servlet
    */
    
    public class CustomServlet 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 CustomServlet...");
        }
    }
    
    /**
    * @Author : parzulpan
    * @Time : 2020-12
    * @Desc : 自定义 Filter
    */
    
    public class CustomFilter 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.println("CustomFilter process...");
            filterChain.doFilter(servletRequest, servletResponse);
        }
    
        @Override
        public void destroy() {
    
        }
    }
    
    /**
    * @Author : parzulpan
    * @Time : 2020-12
    * @Desc : 自定义 Listener
    */
    
    public class CustomListener implements ServletContextListener {
        @Override
        public void contextInitialized(ServletContextEvent servletContextEvent) {
            System.out.println("CustomListener contextInitialized... ");
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent servletContextEvent) {
            System.out.println("CustomListener contextDestroyed... ");
        }
    }
    

  • 自定义服务配置类

    /**
    * @Author : parzulpan
    * @Time : 2020-12
    * @Desc : 自定义服务配置类
    */
    
    @Configuration
    public class CustomServerConfig {
    
        // 自定义 Servlet 添加到容器中
        @Bean
        public ServletRegistrationBean customServlet() {
            ServletRegistrationBean srb = new ServletRegistrationBean(new CustomServlet(), "/customServlet");
            srb.setLoadOnStartup(1);    // 可以设置各种属性
            return srb;
        }
    
        // 自定义 Filter 添加到容器中
        @Bean
        public FilterRegistrationBean customFilter() {
            FilterRegistrationBean frb = new FilterRegistrationBean();
            frb.setFilter(new CustomFilter());
            frb.setUrlPatterns(Arrays.asList("/hello", "/customServlet"));
            return frb;
        }
    
        // 自定义 Listener 添加到容器中
        @Bean
        public ServletListenerRegistrationBean customListener() {
            ServletListenerRegistrationBean<CustomListener> lrb = new ServletListenerRegistrationBean<>(new CustomListener());
            return lrb;
        }
    

比较典型的例子有,SpringBoot 帮我们自动 SpringMVC 的时候,自动的注册 SpringMVC 的前端控制器,即 DispatcherServlet。

更换嵌入式 Servlet 容器

SpringBoot 默认使用 Tomcat 作为嵌入式的 Servlet 容器,它也支持 Jetty 和 Undertow 容器。

Tomcat 是 apache 下的一款重量级的服务器,不用多说历史悠久,而且经得起实践的考验。而 Jetty 和 Undertow 都是基于 NIO 实现的高并发轻量级的服务器,支持 Servlet3.1 和 WebSocket。

ConfigurableEmbeddedServletContainer 继承关系为:

ConfigurableEmbeddedServletContainer继承关系

使用 Jetty

    <dependencies>
        <!-- web 模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- 排除默认的 Tomcat 容器 -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 引入新的 Jetty 容器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
    <dependencies>

使用 Undertow

    <dependencies>
        <!-- web 模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- 排除默认的 Tomcat 容器 -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 引入新的 Undertow 容器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
    <dependencies>

嵌入式 Servlet 容器 自动配置原理

嵌入式 Servlet 容器 怎么配置上去的,怎么工作的?

是因为存在 EmbeddedServletContainerAutoConfiguration 嵌入式的 Servlet 容器自动配置类。

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
//导入 BeanPostProcessorsRegistrar 给容器中导入一些组件
//导入了EmbeddedServletContainerCustomizerBeanPostProcessor,即后置处理器,在 bean 初始化前后(创建完对象,还没赋值赋值)执行初始化工作
@Import(BeanPostProcessorsRegistrar.class)
public class EmbeddedServletContainerAutoConfiguration {

    // 如果正在使用 Tomcat,则为嵌套配置。
    @Configuration
    @ConditionalOnClass({ Servlet.class, Tomcat.class })
    // 判断当前容器没有用户自己定义 EmbeddedServletContainerFactory 嵌入式的 Servlet 容器工厂,它的作用是创建嵌入式的Servlet容器
    @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedTomcat {

        @Bean
        // 返回一个 EmbeddedServletContainer,并且启动 Tomcat服务器,其他容器同理
        public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
            return new TomcatEmbeddedServletContainerFactory();
        }

    }

    // 如果正在使用 Jetty,则为嵌套配置。
    @Configuration
    @ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
    @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedJetty {

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

    }

    // 如果正在使用 Undertow,则为嵌套配置。
    @Configuration
    @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
    @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedUndertow {

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

那么对嵌入式容器的配置修改是怎么生效?

我们知道,对于配置修改有两种方式,一种是修改配置文件,本质是使用 ServerProperties 类,而一种是使用 EmbeddedServletContainerCustomizer 类。值得注意的是,ServerProperties 也是 Customizer,即也是一种定制器。

具体步骤为

  • SpringBoot 根据导入的依赖情况,给容器中添加相应的 EmbeddedServletContainerFactory,比如 TomcatEmbeddedServletContainerFactory 容器工厂
  • 容器中某个组件要创建对象就会使用后置处理器 EmbeddedServletContainerCustomizerBeanPostProcessor,只要是嵌入式的 Servlet 容器工厂,后置处理器就工作。
  • 后置处理器会从容器中获取所有的 EmbeddedServletContainerCustomizer 类,进而调用定制器的定制方法。

嵌入式 Servlet 容器 启动原理

什么时候创建嵌入式的 Servlet 容器工厂?什么时候获取嵌入式的 Servlet 容器并启动 Tomcat?

创建嵌入式的 Servlet 容器工厂过程

创建嵌入式的 Servlet 容器工厂过程 调试断点

创建嵌入式的 Servlet 容器工厂过程

  • 第一点:SpringBoot 应用启动运行 run()
  • 第二点:SpringBoot 刷新 IOC 容器,此时会创建 IOC 容器对象,并初始化容器,创建容器中的每一个组件。如果是 web app 则创建AnnotationConfigEmbeddedWebApplicationContext,否则创建 AnnotationConfigApplicationContext
  • 第三点:刷新刚才创建好的 IOC 容器
  • 第四点:web 的 IOC 容器 重写了 onRefresh()
  • 第五点:web 的 IOC 容器 创建嵌入式 Servlet 容器

获取嵌入式的 Servlet 容器并启动 Tomcat 过程

  • 第一点:获取嵌入式的 Servlet 容器工厂
  • 第二点:使用容器工厂获取嵌入式的 Servlet 容器
  • 第三点:嵌入式的 Servlet 容器创建对象并启动 Servlet 容器
  • 第四点:先启动嵌入式的 Servlet 容器,再将 IOC 容器中剩下没有创建出的对象获取出来

总结的话,就是 IOC容器启动时会创建嵌入式的 Servlet 容器。

使用外置 Servlet 容器

嵌入式 Servlet 容器,可将应用打成可执行的 jar包。

它的优点是:简单、便携;

它的缺点:默认不支持 JSP、优化定制比较复杂;

而外置的 Servlet 容器,就是在外面安装 Tomcat,应用 war 包的方式打包。

使用步骤

  • 必须创建一个 war 项目,然后利用 idea 创建好目录结构
  • 必须将嵌入式的 Tomcat 指定为 provided
  • 必须编写一个 SpringBootServletInitializer 的子类,并调用 configure 方法,服务器依靠它启动 SpringBoot 应用
  • 配置启动 Tomcat 服务器就可以使用

jar包:执行 SpringBoot 主类的 main(),启动 IOC 容器并创建嵌入式的 Servlet 容器,启动 SpringBoot 应用。

war包:启动外置服务器,服务器通过 SpringBootServletInitializer 启动 SpringBoot 应用,启动 IOC 容器并创建Servlet 容器。

本节源码

练习和总结

posted @ 2020-12-29 17:49  Parzulpan  阅读(170)  评论(0编辑  收藏  举报