SpringBoot基础学习之SpringBoot Web开发(中篇)

前言

小伙伴们,大家好,我是狂奔の蜗牛rz,当然你们可以叫我蜗牛君,我是一个学习Java半年多时间的小菜鸟,同时还有一个伟大的梦想,那就是有朝一日,成为一个优秀的Java架构师。
这个SpringBoot基础学习系列用来记录我学习SpringBoot框架基础知识的全过程 (这个系列是参照B站狂神的SpringBoot最新教程来写的,由于是之前整理的,但当时没有发布出来,所以有些地方可能有错误,希望大家能够及时指正!)
之后我将会以一天一更的速度更新这个系列,还没有学习SpringBoot的小伙伴可以参照我的博客学习一下;当然学习过的小伙伴,也可以顺便跟我一起复习一下基础。
最后,希望能够和大家一同进步吧!加油吧!少年们!

由于SpringBoot Web开发涉及的内容比较多,所以蜗牛君打算把这部分将会分成上中下三篇博客,上篇主要分析SpringBoot开发Web的优缺点以及静态资源的配置和使用;中篇主要介绍模板引擎和MVC配置原理,下篇是项目实战,基于SpringBoot构建一个简单的员工管理系统!
SpringBoot Web开发(上篇)博客链接:https://blog.csdn.net/weixin_45301250/article/details/120694674

废话不多说,让我们开始今天的学习内容吧,由于今天我们来到了SpringBoot基础学习的第五站:SpringBoot Web开发(中篇)

5.3 模板引擎

5.3.1 回忆jsp的使用

前端交给我们的页面,是html页面;如果以前开发,需要把它转换成jsp页面

1.jsp的好处

jsp页面的好处

  • 当我们查出一些数据转发到jsp页面以后,我们可以用jsp轻松实现数据的显示和交互等
  • jsp还具有强大功能,包括能写Java代码等
2.jsp的弊端

但是现在使用SpringBoot开发

  • 项目是以jar形式进行打包,而不是war包
  • 使用的是内嵌的Tomcat,默认不支持jsp
3.解决静态页面开发问题
  • SpringBoo默认不支持jsp,如果我们直接用纯静态页面的方式,那么会给开发带来非常大的麻烦
  • 为了解决这个问题,SpringBoot推荐我们使用模板引擎
4.引入模板引擎
4-1 模板引擎种类

jsp其实就是一种模板引擎,还有使用较多的freemarker,包括SpringBoot推荐使用的thymeleaf等

4-2 模板引擎核心思想
  • 模板引擎虽然各式各样,但其核心思想都是基本相同的

在这里插入图片描述

4-2 模板引擎作用

模板引擎作用

比如我们要写一个页面模板,比如有些值是动态的,那么我们可以使用一些表达式

那么这些值从哪里来的呢?

  • 首先我们来组装一些数据,并且把这些数据找到
  • 然后把这些数据交给模板引擎,模板引擎按照数据把表达式解析,填充到指定位置
  • 最后把这个数据最终生成一个我们想要看到的内容渲染出去,这就是模板引擎

不管是jsp还是其他模板引擎,都是这个核心思想;它们之间的不同点,就是语法有些不同

5.3.2 Thymeleaf模板引擎的使用

1.Thymeleaf模板引擎
1-1 什么是Thymeleaf模板引擎?

我们主要学习SpringBoot推荐使用的Thymeleaf模板引擎

  • 这个模板引擎是一个高级语言的模板引擎
  • 并且它的语法很简单,功能也更加强大
1-2 学习方式和网站链接

建议去Thymeleaf官网或者Spring官网学习

网站链接

  • Thymeleaf官网:https://www.thymeleaf.org/
  • Thymeleaf的Github主页:https://github.com/thymeleaf/thymeleaf-spring
  • Spring官方文档:https://docs.spring.io/spring-boot/docs/2.0.3.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-template-engines
2.导入资源依赖和查看依赖版本
2-1 导入thymeleaf资源依赖
<!--引入thymeleaf资源依赖,基于3.x版本-->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
2-2 查看thymeleaf资源依赖版本

在这里插入图片描述

2-3 查看ThymeleafProperties类源码
//使用配置属性注解:绑定指定配置文件中的所有属性值
@ConfigurationProperties(prefix = "spring.thymeleaf")
//Thymeleaf属性类
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/"; //默认前缀
    public static final String DEFAULT_SUFFIX = ".html"; //默认后缀
    private boolean checkTemplate = true; //检查模板
    private boolean checkTemplateLocation = true; //检查模板位置
    private String prefix = "classpath:/templates/"; //前缀
    private String suffix = ".html"; //后缀
    private String mode = "HTML"; //格式
    ......
}
3.Thymeleaf模板引擎的使用
3-1 项目结构

在这里插入图片描述

3-2 编写hello.html页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>test</h1>
</body>
</html>
3-3 编写HelloController控制器类
package com.kuang.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
//在templates目录下的所有页面,只能通过controller来跳转
//这个需要模板应请支持:thymeleaf
//使用@RestController注解来实现Controller接口
@RestController
public class HelloController {
     //真实访问路径为:http://loclahost:8080/hello
    //使用@RequestMapping注解,设置请求映射路径
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}
3-4 测试结果

在这里插入图片描述

结果访问/hello请求,页面跳转成功!

3-5 使用结论
  • 只要需要使用Thymeleaf模板引擎,只需要的导入对应的资源依赖即可
  • 要将html代码放在templates文件下
4.Thymeleaf的使用升级版
4-1 修改hello.index页面
<!DOCTYPE html>
<!--注意:这里要引入thymeleaf的th的约束-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--所欲的html元素都可以被thymeleaf替换接管:th:元素名-->
<div th:text="${msg}"></div>
</body>
</html>
4-2 修改HelloController控制器类
package com.kuang.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
//在templates目录下的所有页面,只能通过controller来跳转
//这个需要模板应请支持:thymeleaf
//使用@Controller注解来实现Controller接口
@Controller
public class HelloController {
    //真实访问路径为:http://loclahost:8080/hello
    //使用@RequestMapping注解,设置请求映射路径
    @RequestMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("msg","Hello,SpringBoot!");
        return "hello";
    }
}
4-3 测试结果

在这里插入图片描述

结果访问/hello请求跳转页面成功!

5.3.3 Thymeleaf的基本语法

1.常用属性和数字优先级
  • 所有的Thymeleaf属性都定义了数字优先级,从而确定了它们在标记中的顺序
顺序参数特性
1th:insert th:repalce碎片夹杂,相当于jsp中include
2th:each片段迭代,for循环遍历
3th:if th:unless th:switch th:case条件评估
4th:object th:with局部变量定义
5th:attr th:attrprepend th:attrappend常规属性修改
6th:value th:href th:src …特定属性修改
7th:text th:utext转译文本和不转译文本
8th:fragement片段规格
9th:remove碎片清除

这种优先机制意味着:如果属性位置反转,则上述迭代片段将会给出完全相同的结果 (尽管可读性稍差)

2.th:untext的使用
2-1 编写HelloController控制器类
package com.kuang.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Arrays;
//在templates目录下的所有页面,只能通过controller来跳转
//使用@Controller注解来实现Controller接口
@Controller
public class HelloController {
    //真实访问路径为:http://loclahost:8080/hello
    //使用@RequestMapping注解,设置请求映射路径
    @RequestMapping("/hello")
    public String hello(Model model) {
        //设置模型:设置msg属性和值,进行数据渲染
        model.addAttribute("msg","<h1>Hello,SpringBoot!</h1>");
        //返回视图逻辑名给视图解析器
        return "hello";
    }
}
2-2 编写hello.html页面
<!DOCTYPE html>
<!--注意:这里要引入th的约束-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--所有的html元素都可以被thymeleaf替换接管:th:元素名-->
<!--使用th:text,不进行文本转义-->
<div th:text="${msg}"></div>
<!--使用th:utext,进行文本转义-->
<div th:utext="${msg}"></div>

</body>
</html>
2-3 测试结果

在这里插入图片描述

3.th:each的使用
3-1 编写HelloController控制器
package com.kuang.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Arrays;
//在templates目录下的所有页面,只能通过controller来跳转
//使用@Controller注解来实现Controller接口
@Controller
public class HelloController {
    //真实访问路径为:http://loclahost:8080/hello
    //使用@RequestMapping注解,设置请求映射路径
    @RequestMapping("/hello")
    public String hello(Model model) {
        //设置模型:设置msg属性和值,进行数据渲染
        model.addAttribute("users", Arrays.asList("周树人","周星驰","周杰伦"));
        //返回视图逻辑名给视图解析器
        return "hello";
    }
}
3-2 编写hello.html页面
  • 方式一
<!DOCTYPE html>
<!--注意:这里要引入th的约束-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--所欲的html元素都可以被thymeleaf替换接管:th:元素名-->
<hr>
<!--使用th:each将数组集合进行遍历,使用th:text将集合项写出-->
<!--方式一:建议使用方式一-->
<h3 th:each="user:${users}" th:text="${user}"></h3>
</body>
</html>
  • 方式二
<!DOCTYPE html>
<!--注意:这里要引入th的约束-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--所欲的html元素都可以被thymeleaf替换接管:th:元素名-->
<hr>
<!--使用th:each将数组集合进行遍历,使用th:text将集合项写出-->
<!--方式二-->
<h3 th:each="user:${users}">[[ ${user} ]]</h3>
</body>
</html>
3-3 测试结果
  • 方式一

在这里插入图片描述

  • 方式二

与方式一结果相同

4.标准表达语法
4-1 简单表达式
  • 变量表达式:${…}
  • 选择变量表达式:*{…}
  • 消息表达:#{…}
  • 链接URL表达式:@{…}
  • 片段表达式:~{…}
4-2 文字
  • 文字文本:‘one text’,‘Another one!’,…
  • 号码文字:0,34,3.0,12.3,…
  • 布尔文字:true,false
  • 空文字:null
  • 文字标记:one,sometext,main,…
4-3 文字操作
  • 字符串串联:+
  • 文字替换:|The name is ${name}|
4-4 算术运算
  • 二元运算符:+,-,*,/,%
  • 减号:(一元运算符):-
4-5 布尔运算
  • 二元运算符:and,or
  • 布尔否定 (一元运算符):!,not
4-6 比较和等值
  • 比较:>,<,>=,<=(gt,It,ge,le)
  • 等号运算符:==,!= (eq,ne)
4-7 条件运算符
  • 如果-则:(if) ? (then)
  • 如果-则-否则:(if) ? (then) : (else)
  • 默认:(value) ?: (defaultVaule)
4-8 特殊令牌
  • 无操作:_
5.Strings工具类的使用
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Strings
 * ======================================================================
 */

/*
 * Null-safe toString()
 */
${#strings.toString(obj)}                           // also array*, list* and set*

/*
 * Check whether a String is empty (or null). Performs a trim() operation before check
 * Also works with arrays, lists or sets
 */
 // 判断字符串名字是否为空
${#strings.isEmpty(name)}
${#strings.arrayIsEmpty(nameArr)}
${#strings.listIsEmpty(nameList)}
${#strings.setIsEmpty(nameSet)}

/*
 * Perform an 'isEmpty()' check on a string and return it if false, defaulting to
 * another specified string if true.
 * Also works with arrays, lists or sets
 */
${#strings.defaultString(text,default)}
${#strings.arrayDefaultString(textArr,default)}
${#strings.listDefaultString(textList,default)}
${#strings.setDefaultString(textSet,default)}

/*
 * Check whether a fragment is contained in a String
 * Also works with arrays, lists or sets
 */
${#strings.contains(name,'ez')}                     // also array*, list* and set*
${#strings.containsIgnoreCase(name,'ez')}           // also array*, list* and set*

/*
 * Check whether a String starts or ends with a fragment
 * Also works with arrays, lists or sets
 */
${#strings.startsWith(name,'Don')}                  // also array*, list* and set*
${#strings.endsWith(name,endingFragment)}           // also array*, list* and set*

/*
 * Substring-related operations
 * Also works with arrays, lists or sets
 */
${#strings.indexOf(name,frag)}                      // also array*, list* and set*
${#strings.substring(name,3,5)}                     // also array*, list* and set*
${#strings.substringAfter(name,prefix)}             // also array*, list* and set*
${#strings.substringBefore(name,suffix)}            // also array*, list* and set*
${#strings.replace(name,'las','ler')}               // also array*, list* and set*

/*
 * Append and prepend
 * Also works with arrays, lists or sets
 */
${#strings.prepend(str,prefix)}                     // also array*, list* and set*
${#strings.append(str,suffix)}                      // also array*, list* and set*

/*
 * Change case
 * Also works with arrays, lists or sets
 */
${#strings.toUpperCase(name)}                       // also array*, list* and set*
${#strings.toLowerCase(name)}                       // also array*, list* and set*

/*
 * Split and join
 */
${#strings.arrayJoin(namesArray,',')}
${#strings.listJoin(namesList,',')}
${#strings.setJoin(namesSet,',')}
${#strings.arraySplit(namesStr,',')}                // returns String[]
${#strings.listSplit(namesStr,',')}                 // returns List<String>
${#strings.setSplit(namesStr,',')}                  // returns Set<String>

/*
 * Trim
 * Also works with arrays, lists or sets
 */
${#strings.trim(str)}                               // also array*, list* and set*

/*
 * Compute length
 * Also works with arrays, lists or sets
 */
${#strings.length(str)}                             // also array*, list* and set*

/*
 * Abbreviate text making it have a maximum size of n. If text is bigger, it
 * will be clipped and finished in "..."
 * Also works with arrays, lists or sets
 */
${#strings.abbreviate(str,10)}                      // also array*, list* and set*

/*
 * Convert the first character to upper-case (and vice-versa)
 */
${#strings.capitalize(str)}                         // also array*, list* and set*
${#strings.unCapitalize(str)}                       // also array*, list* and set*

/*
 * Convert the first character of every word to upper-case
 */
${#strings.capitalizeWords(str)}                    // also array*, list* and set*
${#strings.capitalizeWords(str,delimiters)}         // also array*, list* and set*

/*
 * Escape the string
 */
${#strings.escapeXml(str)}                          // also array*, list* and set*
${#strings.escapeJava(str)}                         // also array*, list* and set*
${#strings.escapeJavaScript(str)}                   // also array*, list* and set*
${#strings.unescapeJava(str)}                       // also array*, list* and set*
${#strings.unescapeJavaScript(str)}                 // also array*, list* and set*

/*
 * Null-safe comparison and concatenation
 */
${#strings.equals(first, second)}
${#strings.equalsIgnoreCase(first, second)}
${#strings.concat(values...)}
${#strings.concatReplaceNulls(nullValue, values...)}

/*
 * Random
 */
${#strings.randomAlphanumeric(count)}

5.4 MVC配置原理

5.4.1 MVC的拓展配置

1. 如何实现MVC拓展配置

如果你想在保持Spring Boot MVC特性的同时,又想添加MVC的拓展配置,例如 Intercepters (拦截器),formatters (格式转换器),view Controller (视图控制器)等,可以在你的自定义类前添加一个@Configuration注解,使你的类成为WebMvcConfigurer,并且不要使用@EnableWebMvc注解

2.编写自定义配置类
package com.kuang.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//扩展SpringMVC 
//使MyMvcConfig成为配置类
@Configuration
//实现WebMvcConfigurer配置接口
public class MyMvcConfig implements WebMvcConfigurer {

}

5.4.2 分析ContentNegotiatingViewResolver类和自定义视图解析器

1.查看ContentNegotiatingViewResolver类
  • 查看ContentNegotiatingViewResolver视图解析器后,发现其实现了ViewResolver接口=
//ContentNegotiatingViewResolver(内容协商视图解析器),它实现了ViewResolver(视图解析器)接口
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean {
    @Nullable
    private ContentNegotiationManager contentNegotiationManager;
    private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
    private boolean useNotAcceptableStatusCode = false;
    @Nullable
    private List<View> defaultViews;
    @Nullable
    private List<ViewResolver> viewResolvers;
    private int order = -2147483648;
    //NOT_ACCEPTABLE_VIEW(不可接收的视图)方法:创建新的视图对象
    private static final View NOT_ACCEPTABLE_VIEW = new View() {
        @Nullable
        //获取内容状态
        public String getContentType() {
            //返回为空
            return null;
        }
        //提交信息方法:其第一个为Map<String, ?>集合对象
        public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
            //响应设置的406状态码
            response.setStatus(406);
        }
    };
}
2.查看resolveViewName方法源码
  • 接着查看ViewResolver接口后,发现其有个resolveViewName方法来解析视图逻辑名,既然ContentNegotiatingViewResolver类实现了ViewResolver接口,那么它一定重写了该方法,我们去查看其是如何进行实现的
//视图解析器接口
public interface ViewResolver {
    @Nullable
    //解析视图逻辑名方法:包含两个参数,第一个是视图逻辑名,第二个是事发地点
    View resolveViewName(String var1, Locale var2) throws Exception;
}
  • 再继续回到ContentNegotiatingViewResolver类中,找到它重写的resolveViewName方法,查看其重写过程
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean {
    ......
    @Nullable
    //解析视图逻辑名方法:有两个参数:viewName是视图逻辑名;locale:地区
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        //获取请求参数对象(RequestAttributes):通过请求上下文容器的获取请求参数方法获取
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        //断言状态是否为attrs(请求参数),如果是就替换ServletRequestAttributes(Servlet请求参数)
        Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
        //获取请求媒介类型数组集合:通过调用getMediaTypes(获取媒介类型)的getRequest(获取请求)方法获取
        List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
        //判断请求媒介类型是否为空
        if (requestedMediaTypes != null) {
            //如果不为空,调用getCandidateViews(获取候选视图),将candidateViews(候选视图)对象存入List<View>(视图集合)中
            List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
            //获取bestView(最佳视图)对象:通过调用getBestView(获取最佳视图)方法获取
            View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
            //判断最佳视图(bestView)对象是否为空
            if (bestView != null) {
                //如果不为空,就返回最佳视图(bestView)对象
                return bestView;
            }
        }
        //获取媒介类型信息:通过判断日志是否能够Debug,并且请求媒介类型对象是否为空,如果能够Debug并且其不为空,则将请求媒介类型对象转换成字符串形式
        String mediaTypeInfo = this.logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";
        //判断它是否使用不可接收的状态码
        if (this.useNotAcceptableStatusCode) {
            //如果使用不可接收状态码,判断日志是否能够Debug
            if (this.logger.isDebugEnabled()) {
                //如果满足条件,打印出406不可接收的提示和媒介类型信息
                this.logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
            }
            //如果不能进行Debug,返则回到不可接收视图方法
            return NOT_ACCEPTABLE_VIEW;
        } else {//如果没有使用不可接收的状态码
        //日志打印debug信息"View remains unresolved"(视图仍然不能被解析)和媒介状态信息
            this.logger.debug("View remains unresolved" + mediaTypeInfo);
            //然后返回空值
            return null;
        }
    }
}
3.查看getCandidateViews方法源码
  • 查看 getCandidateViews获取获选视图方法具体内容
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean {
     ......
//获取候选视图的方法:有三个参数:viewName(视图逻辑名);locale(地区);requestedMediaTypes(请求媒介类型集合),返回值类型是一个封装View视图对象的List集合    
     private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
         //获取候选视图集合对象
         List<View> candidateViews = new ArrayList();
         //判断viewResolver(视图解析器)是否为空
         if (this.viewResolvers != null) {
             //断言状态:contentNegotiationManager(内容协商管理器)对象是否为空
             Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
             //如果不为空,则获取迭代器对象(var5):通过调用视图解析器的iterator方法获取
             Iterator var5 = this.viewResolvers.iterator();
             //判断迭代器中是否存在下一个值
             while(var5.hasNext()) {
                 //如果存在,则通过强制转换获取视图解析器对象
                 ViewResolver viewResolver = (ViewResolver)var5.next();
                 //获取视图对象:通过调用视图解析器的resolveViewName(解析视图逻辑名)方法获取
                 View view = viewResolver.resolveViewName(viewName, locale);
                 //判断视图对象是否为空
                 if (view != null) {
                     //如果不为空,添加视图对象到候选视图集合中去
                     candidateViews.add(view);
                 }
                 //获取请求媒介类型的迭代器var8
                 Iterator var8 = requestedMediaTypes.iterator();
                 //判断请求媒介类型迭代器是否存在下一个值
                 while(var8.hasNext()) {
                     //如果存在,获取其next值,即MediaType对象
                     MediaType requestedMediaType = (MediaType)var8.next();
                     //获取扩展名(extensions)集合:通过调用contentNegotiationManager(内容协商管理器)的resolveFileExtensions(解析文件拓展)方法获取
                     List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);                  //获取拓展名集合的迭代器
                     Iterator var11 = extensions.iterator();
                     //判断拓展名集合的迭代器是否存在下一个值
                     while(var11.hasNext()) {
                         //如果存在next值,通过强制转换获取扩展名
                         String extension = (String)var11.next();
                         //获取带有拓展名的视图逻辑名
                         String viewNameWithExtension = viewName + '.' + extension;
                         //调用视图解析器(viewResolver)对象的解析视图逻辑名(resolveViewName)方法来获取视图对象(view):两个参数,第一参数值为带有拓展名的视图逻辑名,第二参数为是事发地点
                         view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                         //判断视图是否空
                         if (view != null) {
                             //如果不为空,将其添加到候选视图集合中去
                             candidateViews.add(view);
                         }
                     }
                 }
             }
         }
         //判断默认的视图集合是否为空  
         if (!CollectionUtils.isEmpty(this.defaultViews)) {
             //如果默认视图集合不为空,就将它们加入到候选视图集合中去
             candidateViews.addAll(this.defaultViews);
         }
         //如果默认视图集合为空,直接返回候选视图集合即可
         return candidateViews;
     }
     ......
 }
4.自定义视图解析器MyViewResolver
package com.kuang.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//扩展SpringMVC 
//如果你想DIY一些定制化功能,只要使用@Configuration组件,然后将它交给SpringBoot,SpringBoot就会帮我们自动装配
//使MyMvcConfig成为配置类
@Configuration
//实现WebMvcConfigurer配置接口
public class MyMvcConfig implements WebMvcConfigurer {
    //将myViewResovler方法当做组件装入到IOC容器中
    @Bean
    public ViewResolver myViewResovler() {
        return new MyViewResolver();
    }
    //ContentNegotiatingViewResolver实现了视图解析器接口的类,我们就可以把它看做是一个视图解析器
    //自定义一个实现ViewResolver接口的视图解析器MyViewResolver
    public static class MyViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {
            return null;
        }
    }
}

5.4.3 Debug测试观察视图解析器

1.在doService方法上设置断点

在这里插入图片描述

2.查看debug测试结果
  • Debug测试后查看控制台发现分别有this,request和response三个属性

在这里插入图片描述

  • 点击this属性,查看其下的viewResovlers,其中的第一个是默认的内容协商视图解析器,第三个是Thymeleaf的视图解析器,然后第四个就是自定义的MyConfig配置类,其中包含自定义的视图解析器

在这里插入图片描述

3.测试结论
  • SpringBoot在自动配置很多组件时,首先会看容器中有没有用户自己配置的,如果用户使用@Bean注解将一些类注册成组件,那就使用用户配置的,如果没有就使用自动配置的
  • 如果有些组件存在多个,比如我们自定义一个视图解析器,那么SpringBoot就会将用户配置的和默认的进行组合使用

5.4.4 分析DispatcherServlet类源码和FormattingConversionService方法

1.查看doService方法源码
//DispatcherServlet:前置控制器/请求分发器
public class DispatcherServlet extends FrameworkServlet { 
    ......
    //doService执行服务方法
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        this.logRequest(request);
        Map<String, Object> attributesSnapshot = null;
        if (WebUtils.isIncludeRequest(request)) {
            attributesSnapshot = new HashMap();
            Enumeration attrNames = request.getAttributeNames();

            label104:
            while(true) {
                String attrName;
                do {
                    if (!attrNames.hasMoreElements()) {
                        break label104;
                    }

                    attrName = (String)attrNames.nextElement();
                } while(!this.cleanupAfterInclude && !attrName.startsWith("org.springframework.web.servlet"));

                attributesSnapshot.put(attrName, request.getAttribute(attrName));
            }
        }

        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.getWebApplicationContext());
        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
        request.setAttribute(THEME_SOURCE_ATTRIBUTE, this.getThemeSource());
        if (this.flashMapManager != null) {
            FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
            if (inputFlashMap != null) {
                request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
            }

            request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
            request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
        }

        RequestPath previousRequestPath = null;
        if (this.parseRequestPath) {
            previousRequestPath = (RequestPath)request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
            ServletRequestPathUtils.parseAndCache(request);
        }

        try {
            //调用doDispatch方法
            this.doDispatch(request, response);
        } finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
                this.restoreAttributesAfterInclude(request, attributesSnapshot);
            }

            ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
        }
    }
    ......
}
2.查看doDispatch方法
//DispatcherServlet:前置控制器/请求分发器
public class DispatcherServlet extends FrameworkServlet { 
       ......   
       //未响应都会经过doDispatch方法
       protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            try {
                ModelAndView mv = null;
                Object dispatchException = null;

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                    mappedHandler = this.getHandler(processedRequest);
                    if (mappedHandler == null) {
                        this.noHandlerFound(processedRequest, response);
                        return;
                    }

                    HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                    String method = request.getMethod();
                    boolean isGet = "GET".equals(method);
                    if (isGet || "HEAD".equals(method)) {
                        long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                        if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                            return;
                        }
                    }

                    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                        return;
                    }

                    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                    if (asyncManager.isConcurrentHandlingStarted()) {
                        return;
                    }

                    this.applyDefaultViewName(processedRequest, mv);
                    mappedHandler.applyPostHandle(processedRequest, response, mv);
                } catch (Exception var20) {
                    dispatchException = var20;
                } catch (Throwable var21) {
                    dispatchException = new NestedServletException("Handler dispatch failed", var21);
                }

                this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
            } catch (Exception var22) {
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
            } catch (Throwable var23) {
                this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
            }

        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            } else if (multipartRequestParsed) {
                this.cleanupMultipart(processedRequest);
            }

        }
    }
    ......
}
3.查看FormattingConversionService方法源码
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
//WebMVC自动配置类
public class WebMvcAutoConfiguration {      
    ......
      @Bean
      //mvc格式转换服务方法:返回值为格式转换服务(FormattingConversionService)
      public FormattingConversionService mvcConversionService() {
            //获取Format(格式)对象:通过调用mvc属性类(从配置文件中获取)的获取格式方法
            Format format = this.mvcProperties.getFormat();
            //创建WebConversionService(Web转换服务)类实例化对象
            WebConversionService conversionService = new WebConversionService((new DateTimeFormatters()).dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
            //将WebConversionService(Web转换服务)类实例化对象添加到格式中去
            this.addFormatters(conversionService);
            //返回web转换服务实例化对象
            return conversionService;
        }
    ......
}       

5.4.5 使用总结

  • 在SpringBoot中如此多的自动装配,其实它们的原理都是一样的
  • 在通过对WebMvc的自动配置原理进行分析后,也发现其是同样的设计思想
  • 因此一定要养成这样的好习惯:通过查看官方文档,然后再结合源码,来得出相应的结论,这才是学习编程的最佳方式,也是进阶高级程序员的必经之路

5.5.6 自定义视图控制器

1.编写自定义类并且重写视图控制器方法
package com.kuang.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//扩展SpringMVC
//使用@Configuration注解让MyMvcConfig成为配置类
@Configuration
//实现WebMvcConfigurer配置接口
public class MyMvcConfig implements WebMvcConfigurer {
    //视图跳转
    //重写添加视图控制器(addViewControllers)方法
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/test").setViewName("hello");
    }
}

SpringBoot官网介绍说,如果你想要进行扩展你的Spring Boot MVC,那么你可以在你的自定义类前添加一个@Configuration注解,使你的类成为WebMvcConfigurer,并且不要使用@EnableWebMvc注解

但是为什么SpringBoot官网会强调不能使用@EnableWebMvc注解呢?如果我们使用了又会造成怎样的结果呢?让我们做个测试

2.使用@EnableWebMvc注解进行测试
  • 在自定义的配置类前使用@EnableWebMvc注解
package com.kuang.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//扩展SpringMVC
//使用@Configuration注解让MyMvcConfig成为配置类
@Configuration
//使用@EnableWebMvc注解:作用是导入DelegatingWebMvcConfiguration(委派WebMvc配置类),从IOC容器中获取所有的WebMvc配置类
@EnableWebMvc
//实现WebMvcConfigurer配置接口
public class MyMvcConfig implements WebMvcConfigurer {
    //视图跳转
    //重写添加视图控制器(addViewControllers)方法
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/test").setViewName("hello");
    }
}

  • 查看@EnableWebMvc注解的源码,我们发现其导入了一个DelegatingWebMvcConfiguration(委派/授权WebMvc配置类)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
//导入DelegatingWebMvcConfiguration(委派/授权WebMvc配置类)
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
  • 接着查看DelegatingWebMvcConfiguration(委派的WebMvc配置类)源码,我们发现其继承了WebMvcConfigurationSupport(WebMvc配置支持类)
@Configuration(proxyBeanMethods = false)
//DelegatingWebMvcConfiguration(委派的WebMvc配置类)继承了WebMvcConfigurationSupport(WebMvc配置支持类)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    //获取WebMvcConfigurerComposite(WebMvc配置组合)对象
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    //DelegatingWebMvcConfiguration类的无参构造
    public DelegatingWebMvcConfiguration() {
    }
    //自动装配setConfigurers(设置配置类)方法
    @Autowired(required = false)
    //设置配置类方法有一个参数:一个WebMvc配置类数组集合
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        //判断配置类是否为空
        if (!CollectionUtils.isEmpty(configurers)) {
            //如果不为空,将配置类添加到WebMvc配置组合对象中去
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
}
  • 然后我们再次查看WebMvcAutoConfiguration自动配置类,发现其有个条件注解@ConditionalOnMissingBean,即当WebMvcConfigurationSupport类不存在时,下面的所有的自动配置都将会失效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
//当WebMvcConfigurationSupport类不存在时,下面的所有的自动配置都将会失效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
//WebMVC自动配置类
public class WebMvcAutoConfiguration {      
}
3.测试结论

这时我们才突然发现SpringBoot设计的精妙之处,如果我们在自定义的配置类中使用了@EnableWebMvc注解,相当于引入WebMvcConfigurationSupport类,那么就会触发@ConditionalOnMissingBean注解中条件,即WebMvcAutoConfiguration自动配置类失效

4.使用总结

在SpringBoot中,有非常多的xxxConfiguration类帮助我们进行扩展配置,只要看到了,我们就要关注它到底为我们配置了什么


好了,今天的有关 SpringBoot基础学习之SpringBoot Web开发(中篇) 的学习就到此结束啦,欢迎小伙伴们积极学习和讨论,喜欢的可以给蜗牛君点个关注,顺便来个一键三连,我们下期见,拜拜啦!


参考视频链接:https://www.bilibili.com/video/BV1PE411i7CV(【狂神说Java】SpringBoot最新教程IDEA版通俗易懂)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
当前课程中博客项目的实战源码是我在 GitHub上开源项目 My-Blog,目前已有 3000 多个 star:本课程是一个 Spring Boot 技术栈的实战类课程,课程共分为 3 大部分,前面两个部分为基础环境准备和相关概念介绍,第三个部分是 Spring Boot 个人博客项目功能的讲解,通过本课程的学习,不仅仅让你掌握基本的 Spring Boot 开发能力以及 Spring Boot 项目的大部分开发使用场景,同时帮你提前甄别和处理掉将要遇到的技术难点,认真学完这个课程后,你将会对 Spring Boot 有更加深入而全面的了解,同时你也会得到一个大家都在使用的博客系统源码,你可以根据自己的需求和想法进行改造,也可以直接使用它来作为自己的个人网站,这个课程一定会给你带来巨大的收获。作者寄语本课程录制于 2020 年,代码基于 Spring Boot 2.x 版本。到目前为止,Spring Boot 技术栈也有一些版本升级,比如 Spring Boot 2.7 发版、Spring Boot 3.x 版本发布正式版本。对于这些情况,笔者会在本课程实战项目的开源仓库中创建不同的代码分支,保持实战项目的源码更新,保证读者朋友们不会学习过气的知识点。课程特色 课程内容紧贴 Spring Boot 技术栈,涵盖大部分 Spring Boot 使用场景。开发教程详细完整、文档资源齐全、实验过程循序渐进简单明了。实践项目页面美观且实用,交互效果完美。包含从零搭建项目、以及完整的后台管理系统和博客展示系统两个系统的功能开发流程。技术栈新颖且知识点丰富,学习后可以提升大家对于知识的理解和掌握,对于提升你的市场竞争力有一定的帮助。实战项目预览    

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狂奔の蜗牛rz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值