前言

  • 本篇主要分两个部分,第一部分是 springboot 的入门练习,即简单的 hello world 实现,第二部分是以此为例,从实现上对比与 springmvc 开发的差异;
  • 需求:浏览器输入 http://localhost:8080/hello,能够响应页面,上边有文字:hello world;
  • 环境
    • jdb 版本:jdk1.8.0_131;
    • IDE 工具:IntelliJ IDEA 2019.3 community;
    • Maven:apache-maven-3.6.1;
    • 使用 springboot 内嵌的 tomcat;
  • 回顾:springmvc 实现简单的 helloworld

    1.使用 spring boot 实现简单的 helloworld

    1.1 jar 包引入及参数配置

  • jar 包准备

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0"
    3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    5. <modelVersion>4.0.0</modelVersion>
    6. <groupId>com.tjufe.cyt</groupId>
    7. <artifactId>spring-boot-01</artifactId>
    8. <version>1.0-SNAPSHOT</version>
    9. <parent>
    10. <groupId>org.springframework.boot</groupId>
    11. <artifactId>spring-boot-starter-parent</artifactId>
    12. <version>2.2.1.RELEASE</version>
    13. <relativePath/>
    14. </parent>
    15. <dependencies>
    16. <dependency>
    17. <groupId>org.springframework.boot</groupId>
    18. <artifactId>spring-boot-starter-web</artifactId>
    19. </dependency>
    20. </dependencies>
    21. <!-- 这个插件,可以将应用打包成一个可执行的jar包;-->
    22. <build>
    23. <plugins>
    24. <plugin>
    25. <groupId>org.springframework.boot</groupId>
    26. <artifactId>spring-boot-maven-plugin</artifactId>
    27. </plugin>
    28. </plugins>
    29. </build>
    30. </project>
  • 其中 spring-boot-stater-web 可默认引入以下依赖项;

对比 springboot web 开发与 springmvc - 图1

  • 配置文件 application.properties/yml (此处仅涉及端口配置)

    • 注意:如果是 .properties 需设置文件的编码格式

      server.port = 8082
      

      image.png

      1.2 代码编写

      ```java // 1. HelloController @RestController // @RestController 的的作用相当于 @Controller 和 @RespnseBody public class HelloController {

      @RequestMapping(“/hello”) public String hello(){ return “hello world”; } }

//spring boot 总启动入口,@SpringBootApplication 说明该标注类为主配置类,且为一个 spring-boot 应用; @SpringBootApplication public class HelloMainApplication { public static void main(String[] args) { SpringApplication.run(HelloMainApplication.class, args); } }

<a name="onvC5"></a>
## 1.3 结果展示
![image.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584840005317-95306dc2-925e-4ee4-a545-c66099f4836f.png#align=left&display=inline&height=130&name=image.png&originHeight=173&originWidth=726&size=14269&status=done&style=none&width=547)
<a name="GXMeC"></a>
## 1.4 将项目打 jar 包![image.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584083372045-21753d94-0e5d-42f2-b898-15d7e78358b4.png#align=left&display=inline&height=664&name=image.png&originHeight=664&originWidth=1317&size=142536&status=done&style=none&width=1317)
<a name="UI2rh"></a>
## ![image.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584083258230-c48c2d6b-2862-4f1d-9d95-17a7f4d16169.png#align=left&display=inline&height=499&name=image.png&originHeight=499&originWidth=1348&size=62669&status=done&style=none&width=1348)1.5  使用 spring initialize 快速配置

- IDE 都支持使用 Spring 的项目创建向导快速创建一个Spring Boot项目,有不同场景下的功能模块供选择,创建向导会联网创建 Spring Boot 项目;<br />
   - 问题1:遇到“社区版 Intelij IDEA 快速创建一个 spring boot 项目找不到sping Initializer选项”;
      - 解决:下载插件 setting->Plugins->Spring Assistant->restart IDE;
   - 问题2:报错 Artifact contains illegal characters;
      - 解决:artifactId 只能全是小写,单词之间用 - 分割;

![image.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584426036780-f98809f7-4eff-40c0-a2cc-e9b198f0715f.png#align=left&display=inline&height=641&name=image.png&originHeight=641&originWidth=1298&size=63824&status=done&style=none&width=1298)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584428839106-ddfe6d65-209e-46d1-aaaf-347f1264b8f2.png#align=left&display=inline&height=654&name=image.png&originHeight=654&originWidth=1324&size=113413&status=done&style=none&width=1324)
<a name="cEZRd"></a>
# 2. 以 hello 为例,对比 springboot 与 springmvc <br />
<a name="lVIxC"></a>
## 2.1 对比目录结构
![mvc1.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584842455552-c935059a-57db-4c89-bc69-2a09d8631ce1.png#align=left&display=inline&height=321&name=mvc1.png&originHeight=439&originWidth=805&size=29908&status=done&style=none&width=588)

- 目录结构的生成:springboot 通过spring assistant 自动生成,springmvc 通过 maven-webapp 自动生成;
- 共同点:运行一个项目,包括存 java 类的文件夹、resources 文件夹、参数配置文件 spplication.properties、导入 jar 包的 pom.xml;
- 区别(由上而下分析):
   - 主运行程序
      - spring mvc 较 springboot 没有 springbootWebApplication 主运行函数,使用前需新建并配置当前项目 tomcat server 服务依赖;
   - resources 文件
      - springboot 的 resources 文件夹下有四个默认子文件夹路径,用于存放不同类型的资源,其中 template 用于存放返回的视图文件 .html;
      - springmvc 的 resouces 文件夹主要针对的是 spring-application 和 springmvc 的配置,WEB-INF 下的 web.xml 是针对 tomcat 的 web 配置;
   - WEB-INF 文件夹
      - springmvc 的目录保持着对 tomcat 的格式依赖,其中 web.xml 用于配置 dispatcherServlet 和 contextListener,并相应给出 springmvc.xml 和 applicationContext.xml 的匹配路径名,另外也存储响应的视图等资源,但需在 springmvc 中说明存储路径;
      - springboot 没有 WEB-INF,但是与 web 相关的资源都存储在 resources 文件夹下;
   - springmvc 的单元测试函数需手动生成,而 springboot 由创建向导自动生成;
<a name="iwcHv"></a>
## 2.2 导包方式与资源配置
<a name="fTHzN"></a>
### 2.2.1  pom.xml 导入依赖包

- springmvc 将所有的包不加区分的全写在 pom.xml 中,而 springboot 只需写明我们需要哪个启动场景和对应的版本号就会自动识别并导入项目开发中所依赖的包;
- springboot 就是在 springmvc 的基础上按应用场景做一个归档,而且自动定义和封装了 jar 包之间依赖关系,而且这种封装一定程度上也避免了自己组装 jar 包可能带来的版本冲突问题; 

![mvc1.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584848476856-e76053ec-ed97-4d11-8d57-cbf494722f15.png#align=left&display=inline&height=1170&name=mvc1.png&originHeight=1170&originWidth=2086&size=155395&status=done&style=none&width=2086)
<a name="Wxrmg"></a>
### 2.2.2 文件配置

- 在之前的 springmvc 中,需要配置的文件一般有 web.xml、springmvc.xml、applicationContext.xml;
   - 在 web.xml 可进行 Listener、Filter、Servlet 配置等;
   - 在 springmvc.xml 可规定要扫描添加到 applicationContext 中的 controller 组件,进行视图解析器、过滤器、拦截器、类型转换器等配置;
   - 在applicationContext.xml 可规定要添加扫描到 applicationContext 中的除 controller 以外的组件,此外还可以进行数据库的连接工作;
- 对于 web 项目开发,springboot 在 springmvc 还夹杂着配置文件的基础上使用 spring 注解和配置类进一步去 xml 化,并在springApplication.run() 启动过程中自动依据配置类生成各种容器和一些默认的通用组件,以简化原本的 springmvc 开发;
<a name="7guen"></a>
#### 2.2.2.1 对比 web.xml 与 ServletWebServerFactoryAutoConfiguration

- springboot 中与 web.xml 功能一致的是 ServletWebServerFactoryAutoConfiguration 配置类,此外还包括异常处理器、分发器等对应的 config 类(ErrorMvcAutoConfiguration、DispatcherServletAutoConfiguration 等),这些配置类都放在 META-INF/spring.factories 中,会在 springboot 启动时自动加载配置类并初始化;

![image.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584956650549-15952963-7afa-48d5-b0cc-639270c8ec0e.png#align=left&display=inline&height=240&name=image.png&originHeight=240&originWidth=1056&size=66463&status=done&style=none&width=1056)

- 这些 config 类的特点
   - 配置类的加载顺序
      - @AutoConfigureBefore({WebMvcAutoConfiguration.class})
      - @AutoConfigureAfter({WebMvcAutoConfiguration.class})
   - 如何找到可以修改的默认参数值
      - @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})

![image.png](https://cdn.nlark.com/yuque/0/2020/png/611598/1584957432695-be4982cc-4e36-438b-852b-8a2fc6197049.png#align=left&display=inline&height=82&name=image.png&originHeight=82&originWidth=779&size=12662&status=done&style=none&width=779)

- 如何定制自己的组件
   - 一般通过 ServletContextInitializer 接口的实现类 RegistrationBean,可定制自己所需要的 filter、listener 和 servlet 等,具体可针对某个特定功能的组件,通过继承其子类来实现;
```java
// ServletContextInitializer 主要被 RegistrationBean 实现
public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
    ...
}
//RegistrationBean 有以下继承子类,是 ServletContext 容器中的抽象组件
    AbstractFilterRegistrationBean
    DelegatingFilterProxyRegistrationBean
    DispatcherServletRegistrationBean
    DynamicRegistrationBean
    FilterRegistrationBean
    ServletListenerRegistrationBean
    ServletRegistrationBean
  • 案例

    //@EnableWebMvc 使用该注解会使默认的 WebMvcAutoConfiguration 失效,只启用自定义的 MyMvcConfig
    @Configuration
    public class MyMvcConfig  implements WebMvcConfigurer {
      @Bean
      public WebMvcConfigurer webMvcConfigurer(){
          WebMvcConfigurer webMvcConfigurer = new WebMvcConfigurer() {
              //添加视图解析器
              @Override
              public void addViewControllers(ViewControllerRegistry registry) {
                  registry.addViewController("/").setViewName("login");
                  registry.addViewController("/index").setViewName("login");
                  registry.addViewController("/index.html").setViewName("login");
                  registry.addViewController("/main.html").setViewName("dashboard");
              }
              //注册拦截器
              @Override
              public void addInterceptors(InterceptorRegistry registry) {
                  //spring boot 已经做好了静态资源映射,所以不用像 spring mvc 一样进行此操作
                  registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("/", "index", "/index.html", "/user1/login", "/asserts/**");
              }
          };
          return webMvcConfigurer;
      }
    
      @Bean
      public LocaleResolver localeResolver(){
          return new MyLocalResolver();
      }
    }
    

    2.2.2.2 对比 springmvc.xml 与 WebMvcConfigurer 接口

  • springboot 中与 springmvc.xml 功能一致的类是 WebMvcConfigurer 接口,可通过实现该接口定制mvc 形式的 web 开发中可能用到的视图解析器、过滤器、拦截器等;

  • 参数修改可见 WebMvcProperties.class、TaskExecutionProperties.class

    public interface WebMvcConfigurer {
      default void configurePathMatch(PathMatchConfigurer configurer) {
      }
    
      default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
      }
    
      default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
      }
    
      default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
      }
    
      default void addFormatters(FormatterRegistry registry) {
      }
    
      default void addInterceptors(InterceptorRegistry registry) {
      }
    
      default void addResourceHandlers(ResourceHandlerRegistry registry) {
      }
    
      default void addCorsMappings(CorsRegistry registry) {
      }
    
      default void addViewControllers(ViewControllerRegistry registry) {
      }
    
      default void configureViewResolvers(ViewResolverRegistry registry) {
      }
    
      default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
      }
    
      default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
      }
    
      default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
      }
    
      default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
      }
    
      default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
      }
    
      default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
      }
    
      @Nullable
      default Validator getValidator() {
          return null;
      }
    
      @Nullable
      default MessageCodesResolver getMessageCodesResolver() {
          return null;
      }
    }
    
  • 案例

    //1. 注册三大组件:myServlet、myFilter、myListener
    @Configuration
    public class MyServerConfig{
      @Bean
      public ServletRegistrationBean myServlet(){
          ServletRegistrationBean registrationBean = new ServletRegistrationBean(new MyServlet(), "/myServlet");
          return registrationBean;
      }
      @Bean
      public FilterRegistrationBean myFilter(){
          FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
          filterRegistrationBean.setFilter(new MyFilter());
          filterRegistrationBean.setUrlPatterns(Arrays.asList("/hello", "/myServlet"));
          return filterRegistrationBean;
      }
      @Bean
      public ServletListenerRegistrationBean myListener(){
          ServletListenerRegistrationBean<MyListener> listener = new ServletListenerRegistrationBean(new MyListener());
          return listener;
      }
      //配置嵌入式的 servlet 容器
      @Bean
      public WebServerFactoryCustomizer embeddedServletContainerCustomizer(){
          return new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>(){
              @Override
              public void customize(ConfigurableServletWebServerFactory factory) {
                  factory.setPort(8083);
              }
          };
      }
    }
    //2. 自定义的三大组件
    //2.1 自定义 MyServlet
    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");
      }
    }
    //2.2 自定义 MyFilter
    public class MyFilter implements Filter {
      @Override
      public void init(FilterConfig filterConfig) throws ServletException {
          System.out.println("myFilter 初始化");
      }
      @Override
      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
          System.out.println("myFilter process...");
          filterChain.doFilter(servletRequest, servletResponse);
      }
      @Override
      public void destroy() {
    
      }
    }
    //2.3 自定义 MyListener
    public class MyListener implements ServletContextListener {
      @Override
      public void contextInitialized(ServletContextEvent sce) {
          System.out.println("myListener, web 应用启动...");
      }
      @Override
      public void contextDestroyed(ServletContextEvent sce) {
          System.out.println("myListener, web 应用销毁...");
      }
    }
    

    2.2.2.3 springboot 一站式+开箱即用

  • springboot 支持对应场景 stater 下的默认配置+定制化;

对比 springboot web 开发与 springmvc - 图3

2.3 处理请求的过程

image.png

2.3.1 DispatcherServlet

  • dispatcherServlet 是作为 spring web 应用的 front controller 前端控制器,继承自servlet,拦截/* 的请求,然后根据请求的uri,将请求分发给我们的application controller(command)去处理具体的请求;
  • springmvc 中在 web.xml 中配置

对比 springboot web 开发与 springmvc - 图5

  • DispatcherServletAutoConfiguration 配置类里直接以 @bean 的方式注入

image.png

  • 如果要对默认的 dispatcherServlet 做修改,可参照 WebMvcProperties.class 中的静态类 servlet;

image.png

2.3.2 HandlerMapping 与 HandlerAdapter

  • springmvc 在解析 springmvc.xml 时通过开启注解驱动: 自动将 HandlerMapping 与 HandlerAdapter 的 bean 加入到容器中,且驱动元素通过 AnnotationDrivenBeanDefinitionParser 解析;

    2.3.2.1 URL 映射-HandlerMapping

  • 请求任何的 URL 都会被相应的 HandlerMapping 映射到对应的 Handler 上处理,即使最坏的 404,也会被SimpleUrlHandlerMapping 映射到 ResourceHttpRequestHandler 中处理;。

  • HandlerMappings(部分说明)
    • SimpleUrlHandlerMapping
      • 在 WebMvcAutoConfiguration 的内部配置类 FaviconConfiguration 中声明,匹配的 URL 路径是 **/favicon.ico,作用是响应浏览器获取收藏夹中的 icon 文件;
      • 在WebMvcAutoConfiguration 的内部配置类 EnableWebMvcConfiguration 的父类 WebMvcConfigurationSupport 中声明;
    • RequestMappingHandlerMapping:在 WebMvcAutoConfiguration 的内部配置类EnableWebMvcConfiguration 中声明,和 XML 的注解驱动一样,匹配在 Controller中 @RequestMapping注解指定的 URL;
    • WelcomePageHandlerMapping:在 WebMvcAutoConfiguration 的内部配置类WebMvcAutoConfigurationAdapter 中声明,将 URL 路径是 / 或 /index 映射到欢迎页面;
    • BeanNameUrlHandlerMapping:以 bean 的名称,即 beanID 作为 URL,由匹配该 URL 的 Controller 处理业务,且该 Controller 必须继承 AbstractController;【这点现在还不是很懂。。。】
    • resourceHandlerMapping:专门用于映射资源文件;

mvc1.png

2.3.2.2 URL 适配器-HandlerAdapter

2.3.3 ViewResolver

  • ViewResolver 的作用就是通过解析 ModelAndView,将 ModelAndView 中的逻辑视图名映射为 View 对象,并将 ModelAndView 中的 Model 取出;
  • spring 框架中有的视图解析器 详见
    • ViewResolverComposite 简单来说就是使用简单的List来保存你配置使用的视图解析器
    • InternalResourceViewResolver
    • ContenNegotiating - 《Spring MVC Content Negotiation》
    • BeanNameViewResolver - 对 实现 View 接口 的 Bean 做 BeanName 映射
    • DefaultErrorViewResolver - 做 Error 情况 的 视图渲染
  • springboot 的视图解析

springboot推举使用的视图解析器是ThymeleafViewResolver,其一般用来解析的视图类型是html。使用官方推举的目录结构适合用来搭配Thymeleaf视图解析器,如果要改用InternalResourceViewResolver解析器,会相对要麻烦一些。
需要注意的是,如果同时引入了这两中类型的依赖,会以thymeleaf的优先级更高,会造成jsp解析失败。
springboot不支持从静态资源直接请求jsp文件!即使配置了与其对应的视图解析器。
springmvc配置InternalResourceViewResolver解析器会相对简单,而配置Thymeleaf视图解析器会相对麻烦一些。

2.4 不同打包方式下的启动原理

2.4.1 如何实现不同打包

  • springmvc 一般生成 war 包

    <!--pom.xml-->  
    <groupId>cn.tjufe</groupId>
    <artifactId>springmvc_1</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>
    
  • springboot 可打成 jar 包

    <!--pom.xml-->      
    <build>
          <plugins>
              <plugin>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-maven-plugin</artifactId>
              </plugin>
          </plugins>
      </build>
    
    • springboot 项目发布的时候,目前大多数的做法还是排除内置的tomcat,打 war 包,然后部署在生产的tomcat 中;
      <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>
      <!--添加servlet-api依赖--->
      <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
      </dependency>
      

      2.4.2 启动原理对比

  • jar 包:执行 springboot 主类的 main 方法,启动 ioc 容器,创建嵌入式的 servlet 容器;

  • war 包:启动服务器,服务器启动 springboot 应用【SpringBootServletInitializer】,启动ioc容器;

    2.5 端口监听

  • springboot 由于是内置 web 服务器,所以一般一个 web 应用就对应于一个端口号,且其端口号由应用自行决定,端口的监听也由应用自己进行;

  • springmvc 应用的运行是基于外部 tomcat 的,所以该应用所配置的端口号是由 tomcat 来决定的,继而一个端口下可能存在多个 web 应用,使得对于每个 web 应用的访问要通过各个应用的根目录来区分,端口的监听也由tomcat 进行;
  • 小结

    • springboot 开发的 web 应用与端口是一对一的关系,springmvc 开发的 web 应用与端口是多对一关系;

      3.总结与思考

  • springboot 是一只吞下 springmvc 的大鱼;

  • spring boot 隐式实现了 springmvc 这套逻辑的一个默认配置,充分发挥 @Controller、@Configuration 等注解的作用,保证了配置容器的可扩展性;

    参考

    Spring、SpringMVC和SpringBoot看这一篇就够了!
    [spring boot源码解析] HandlerMapping从何而来
    关于SpringBoot与SpringMVC的差异性