封面画师:adsuger     封面ID:78096650

0. 前沿

创建一个 SpringBoot Web 项目的步骤:

  1. 创建一个 SpringBoot 应用,选择我们需要的模块,SpringBoot 会自动把我们选择的模块配置好。

  2. 在配置文件(properties 或 yml)中手动进行配置

  3. 编写业务代码!完事!✌️


至于 SpringBoot 帮我们配置了什么?我们又该怎么在配置文件中编写自己的配置?

这些问题都可以在 SpringBoot 原理 一文中查看!

注意:本文暂不涉及 SpringBoot 与数据库的使用!

1. 静态资源

在一个 Web 项目中,会有很多的静态资源,比如:.js 文件、.css 文件、或者一些图片,那么这些静态资源在 SpringBoot 的 Web 项目中应该怎么处理呢?

要进行 Web 项目的编写,就需要用到 Spring MVC。在 SpringBoot 中,Spring MVC 的配置都在配置类 WebMvcAutoConfiguration 中,我们前往依赖中,找到 SpringBoot 自动配置的依赖(spring-boot-autoconfigure),打开 META-INF 目录下的 spring.factories 文件(这个文件已经在 SpringBoot 原理 一文中详细介绍了 )。

在 spring.factories 文件中找到自动配置类 web.servlet.WebMvcAutoConfiguration,我们点击并打开它。在这个类中,我们找到 WebMvcAutoConfigurationAdapter 类,找到这个类后我们往下拉,可以看到方法—— addResourceHandlers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 禁用默认资源处理
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    // 缓存控制
    Duration cachePeriod = 
        this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().
        getCachecontrol().toHttpCacheControl();
    // webjars 配置
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    // 静态资源处理
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

webjars 配置 的代码下,我们可以看到:所有的/webjars/** 都需要去 classpath:/META-INF/resources/webjars/目录下寻找资源。

1.1 WebJars 配置

WebJars 就是以 jar 包的方式导入静态资源,以前我们需要手动导入的静态资源(比如:jquery.js),现在我们可以在 pom.xml 导入依赖就可以了,比如:

1
2
3
4
5
6
<!--导入jQuery的依赖-->
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.2.1</version>
</dependency>

PS:相关依赖可以在 WebJars 官网找到:WebJars

我们可以查看导入的 jQuery 的目录结构:

webjars-jQuery目录结构

既然使用 webjars 就相当于导入了静态资源,那我们启动项目后可以访问到静态资源吗?当然可以,我们可以打开项目,在地址栏输入 http://localhost:8080/webjars/jquery/3.2.1/jquery.js,即可看到如下界面:

webjars-地址栏访问jQuery

1.2 静态资源映射

从源码中,我们可以看到,除了可以使用 webjars 导入依赖,还可以使用静态资源映射。

  • 我们点击 resourceProperties 常量,可以在 WebMvcAutoConfigurationAdapter 类中找到 ResourceProperties

resourceProperties变量

  • 点击并进入 ResourceProperties 类,在这个类的首行就可以看到一个数组常量:
1
2
3
4
5
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
    "classpath:/META-INF/resources/",
    "classpath:/resources/",
    "classpath:/static/",
    "classpath:/public/" };

ResourceProperties 可以设置与静态资源相关的参数,指向会去寻找资源的文件夹,即上面数组的内容。所以,我们不难得出结论,以下的四个目录存放的静态资源可以被我们识别到:

1
2
3
4
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"

同样,我们可以在 resources 根目录下创建文件夹,然后我们就可以把我们的静态资源放到这些文件夹里了。比如,我们创建了一个 myResource.js 文件,然后我们启动项目,访问 http://localhost:8080/myResource.js,就可以访问到静态资源了。

我们还可以测试这三个文件夹下静态资源的访问顺序:我们在 src/main/resources 目录下创建文件夹 resources 和 public,然后在 resources、public、static 三个文件夹 类编写一个.js 文件,然后启动项目,在地址栏进行访问,访问后可得这三个文件夹下静态资源的访问顺序为:

resources > static > public

templates 目录的使用在下文有所介绍!

1.3 自定义静态资源路径

除了前面两种方式设置静态资源路径,我们还可以自定义静态资源访问路径。操作也很简单,只需要在配置文件中编写配置就可以了,比如(application.properties):

1
spring.resources.static-locations=classpath:/yang/,classpath:/mofan/

但需要注意:一旦自定义了静态资源的路径,SpringBoot 自动配置的静态资源路径都将失效!

2. 首页和图标定制

首页为重点!

首页

我们在进行 Web 开发时,首先就需要解决首页的问题,那么 SpringBoot 中,应该怎么设置首页呢?

与静态资源处理一样,我们前往 spring.factories 文件中找到自动配置类 web.servlet.WebMvcAutoConfiguration,点击并打开它。我们在 WebMvcAutoConfiguration 类中,直接搜索 welcome 关键字。然后可以找到如下的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
                                                           FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
        new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
        this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    return welcomePageHandlerMapping;
}

private Optional<Resource> getWelcomePage() {
    String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}

// 首页就是location下的index.html
private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

private boolean isReadable(Resource resource) {
    try {
        return resource.exists() && (resource.getURL() != null);
    }
    catch (Exception ex) {
        return false;
    }
}

这几个方法都是与首页相关的。我们将重点放在 getIndexHtml 方法,这个方法用来配置首页,表示 location 下的 index.html 就是首页,那么 location 又是什么呢?

我们在 getIndexHtml() 上面可以看到 getWelcomePage(),这个方法调用了 getIndexHtml()。然后,在 getWelcomePage() 中我们又发现如下代码:

1
String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());

这一行代码就表示了 location 的含义。我们点击并进入 getStaticLocations()

1
2
3
public String[] getStaticLocations() {
    return this.staticLocations;
}

点击 staticLocations,找到这个变量:

1
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

哦?这是个数组变量?😳

CLASSPATH_RESOURCE_LOCATIONS 这个常量好像很熟悉?对,这个就是我们在上文看到的用于表示静态资源文件夹位置的常量:

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

这样一看,那我们的首页 index.html 就可以存放在这些位置了。👍

然后,我们可以创建一个 index.html,把这个文件放在上述的静态资源文件夹下,然后启动项目,就可以用 http://localhost:8080/ 访问到首页了。

但是真正开发中,我们不会直接在地址栏输入 URL 进行访问,我们会使用 Controller 来访问,就像 Spring MVC 中一样。

我们在此直接编写 Controller 是不能访问到 index.html 的,为什么?我们知道 Spring MVC 是需要配置视图解析器,但在这我们并没有配置,所以是不能够访问。除此之外,SpringBoot 官方建议使用模板引擎 Thymeleaf,而不是继续沿用.jsp 文件,所以,在 SpringBoot 中,如果我们想要访问页面,导入 Thymeleaf 的依赖就可以了导入 Thymeleaf 后会自动向 Spring 容器中添加一个视图解析器

图标定制

最新版的 SpringBoot 中,已经取消设置 icon 功能!(2.2.4 版本已取消)

❓ 那么我们想要定制图标该怎么做呢?

在 static 目录下,创建 images 文件夹,将图标放在这个文件夹下(个人习惯,但也建议这么做!),然后直接在界面使用 <link> 应用:

1
<link rel="icon" type="image/x-icon" href="/images/xx.icon" />

关闭默认图标(在配置文件中编写):

1
2
#关闭默认图标
spring.mvc.favicon.enabled=false

3. Thymeleaf 模板引擎

3.1 模板引擎

❓ 什么是模板引擎?

模板引擎是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的 HTML 文档。


在以前,我们在.jsp 文件中书写 Java 代码、HTML 标签,甚至 CSS、JS 代码,.jsp 文件似乎很强大。确实很强大,可代码的耦合度却很高。不仅如此,我们现在使用 SpringBoot 后,项目将以 jar 的方式进行打包,不再是 war,同时我们还使用的是内嵌的 tomcat,因此,SpringBoot 现在默认是不支持 jsp 的

既然不支持,总的想个办法替代 jsp 吧,因此,SpringBoot 推荐你使用模板引擎。比如:Thymeleaf。

简单理解一下模板引擎的作用:将网页中动态的数据与静态的页面通过模板引擎生成 HTML 代码。

众多模板引擎中,SpringBoot 推荐使用 Thymeleaf,那么这玩意该咋用呢?

3.2 引入 Thymeleaf

首先,我们得引入 Thymeleaf,在 SpringBoot 中,就是一个 starter 的事,我们在 pom.xml 中引入它:

1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

这样,引入就完成了!简直不要太爽!👊

引入是完成了,那又该咋用呢?

3.3 使用 Thymeleaf

先记住一句话:使用了 Thymeleaf 模板引擎的.html 文件都放在 templates 目录下

但是在实际使用的时候要记得:我们无法直接访问 templates 目录下的文件,但可以使用 Controller 进行访问。

我们又来看源码,全局搜索 ThymeleafProperties 配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
    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 = DEFAULT_PREFIX;
    private String suffix = DEFAULT_SUFFIX;
    private String mode = "HTML";
    private Charset encoding = DEFAULT_ENCODING;
    private boolean cache = true;
    // ...
}

我们可以看到默认设置的前缀和后缀,这就是前文说的:为什么没有配置视图解析器,但是导入 Thymeleaf 依赖就可以使用 Controller 访问界面了。

同时,我们还可以看到默认设置的文件路径,这就是为啥开头要说「使用了 Thymeleaf 模板引擎的.html 文件都放在 templates 目录下」的原因了。

我们不妨测试一手?

  • 编写一个 Controller:
1
2
3
4
5
6
7
8
9
@Controller
public class HelloController {

    @RequestMapping("/aa")
    public String hello(){
        return "hello";
    }

}
  • 编写测试界面 hello.html,记得放在 templates 目录下:
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>你好</h1>
</body>
</html>
  • 启动项目,在地址栏输入 localhost:8080/aa 试试?

界面是可以访问了,难道 Thymeleaf 就充当一个视图解析器的功能?当然不是!👇

3.4 Thymeleaf 语法基础使用

不同的模板引擎有不同的语法,那 Thymeleaf 的语法是怎样的呢?

我们可以查看官方文档:Thymeleaf官网

在此,给出一个简单的使用:

  • 首先,需要在.html 文件中导入命名空间约束:
1
2
<html lang="en" xmlns:th="https://www.thymeleaf.org/">
<!-- xmlns:th="https://www.thymeleaf.org/" 就是约束 -->
  • 编写 Controller:
1
2
3
4
5
@RequestMapping("/test")
public String test(Model model){
    model.addAttribute("msg","Hello,Thymeleaf");
    return "test";
}
  • 编写测试界面 test.html,记得放在 templates 目录下:
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>我是测试界面</h1>
    
<!-- Thymeleaf语法的使用 -->
<div th:text="${msg}"></div>
</body>
</html>
  • 启动项目,在地址栏输入 localhost:8080/test 试试?

我们已经会了最基本的语法了,那 Thymeleaf 还有其他的语法吗?当然是有的!💪

3.5 Thymeleaf 高级语法

高级语法有哪些呢?官方文档走起!

前往文档第 10 点,查看属性优先级:

我们可以使用这些属性来替换 HTML 中原生的属性!

Attribute-Precedence

那么我们又可以写那些表达式呢?

依旧是官方文档!前往文档第 4 点,查看标准表达语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Simple expressions:
    Variable Expressions: ${...}    (获取变量值)
    {...}可以书写的内容有:
        1. 获取对象的属性、调用方法,使用OGNL表达式
        2. 内置的表达式基本对象
            #ctx: the context object.
            #vars: the context variables.
            #locale: the context locale.
            #request: (only in Web Contexts) the HttpServletRequest object.
            #response: (only in Web Contexts) the HttpServletResponse object.
            #session: (only in Web Contexts) the HttpSession object.
            #servletContext: (only in Web Contexts) the ServletContext object.
        3. 内置的表达工具对象
            #execInfo: information about the template being processed.
            #messages: methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
            #uris: methods for escaping parts of URLs/URIs
            #conversions: methods for executing the configured conversion service (if any).
            #dates: methods for java.util.Date objects: formatting, component extraction, etc.
            #calendars: analogous to #dates, but for java.util.Calendar objects.
            #numbers: methods for formatting numeric objects.
            #strings: methods for String objects: contains, startsWith, prepending/appending, etc.
            #objects: methods for objects in general.
            #bools: methods for boolean evaluation.
            #arrays: methods for arrays.
            #lists: methods for lists.
            #sets: methods for sets.
            #maps: methods for maps.
            #aggregates: methods for creating aggregates on arrays or collections.
            #ids: methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).
          4. 格式化日期
==============================================================================================================================================================
    Selection Variable Expressions: *{...}        (功能与取值表达式一样)
    Message Expressions: #{...}        (获取国际化内容)
    Link URL Expressions: @{...}
    Fragment Expressions: ~{...}    (片段引用表达式)
    
Literals
    Text literals: 'one text', 'Another one!',…
    Number literals: 0, 34, 3.0, 12.3,…
    Boolean literals: true, false
    Null literal: null
    Literal tokens: one, sometext, main,…
    
Text operations:
    String concatenation: +
    Literal substitutions: |The name is ${name}|
    
Arithmetic operations:
    Binary operators: +, -, *, /, %
    Minus sign (unary operator): -
    
Boolean operations:
    Binary operators: and, or
    Boolean negation (unary operator): !, not
    
Comparisons and equality:
    Comparators: >, <, >=, <= (gt, lt, ge, le)
    Equality operators: ==, != (eq, ne)
    
Conditional operators:
    If-then: (if) ? (then)
    If-then-else: (if) ? (then) : (else)
    Default: (value) ?: (defaultvalue)
    
Special tokens:
    No-Operation: _

PS:

转义与不转义:

1
2
3
<div th:text="${msg}"></div>
<!--不转义-->
<div th:utext="${msg}"></div>

行内写法与行内写法:

1
2
3
4
5
<h4 th:each="user :${users}" th:text="${user}"></h4>

<h4>
    <span th:each="user:${users}">[[${user}]]</span>
</h4>

4. MVC 自动配置

我们要使用 SpringBoot 编写网页,那么使用 MVC 是必要的。SpringBoot 的核心思想就是 约定大于配置,既然如此,想必 SpringBoot 也一定配置了 MVC。

那么我们应该怎么使用呢?

遇事不决,读文档!👊

SpringBoot 2.2.4 MVC自动配置参考文档

为了避免链接挂了,我截了个图:

SpringMVCAuto-configuration

你会发现,我把截图中的某一处用红色矩形圈出,具体原因后面分析!

4.1 内容协商视图解析器

我们按照文档的顺序,先来看看 ContentNegotiatingViewResolver (内容协商视图解析器)。

进入 WebMvcAutoConfiguration 类,然后搜索 ContentNegotiatingViewResolver

1
2
3
4
5
6
7
8
9
10
11
@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
    // ContentNegotiatingViewResolver uses all the other view resolvers to locate
    // a view so it should have a high precedence
    resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return resolver;
}

在注释里说:内容协商视图解析器使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级。

我们再点击 ContentNegotiatingViewResolver 进入这个类,在这个类下有这样一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    if (requestedMediaTypes != null) {
        // 获取候选的视图解析器
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        // 选择一个合适的视图解析器,然后把这个对象返回
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }

    String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
        " given " + requestedMediaTypes.toString() : "";

    if (this.useNotAcceptableStatusCode) {
        if (logger.isDebugEnabled()) {
            logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
        }
        return NOT_ACCEPTABLE_VIEW;
    }
    else {
        logger.debug("View remains unresolved" + mediaTypeInfo);
        return null;
    }
}

在上面的代码中,我们可以看到 获取了候选的试图解析器,那么是怎么获取的呢?我们点击并进入 getCandidateViews

在这个方法中,简单概括就是:遍历所有的视图解析器,然后将它们封装成一个对象,然后将对象添加到候选的视图,并返回候选的视图。

不难得出结论: ContentNegotiatingViewResolver 是用来组合所有的视图解析器的


我们再研究一下源码:在 getCandidateViews 会发现有一个 viewResolvers,我们已经知道在 getCandidateViews 中会遍历视图解析器,那么视图解析器是在哪里赋值的呢?

ContentNegotiatingViewResolver 类中,有这样一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void initServletContext(ServletContext servletContext) {
    Collection<ViewResolver> matchingBeans =
        BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList<>(matchingBeans.size());
        for (ViewResolver viewResolver : matchingBeans) {
            if (this != viewResolver) {
                this.viewResolvers.add(viewResolver);
            }
        }
    }
    // ...
}

在这个方法中,我们可以看到使用了一个工具类获得视图解析器,那么我们是否可以自己编写一个视图解析器,然后把这个视图解析器添加到容器呢?👇


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Bean //注册到容器中
    public ViewResolver myViewResolver(){
        return new MyViewResolver();
    }

    //实现ViewResolver接口
    // 实现了视图解析器接口的类,我们都可以把它当作视图解析器
    private static class MyViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {
            return null;
        }
    }
}

编写完成后,我们全局搜索 DispatcherServlet 中的 doDispatch(),并给它打个断点:

doDispatch

然后使用 Debug 模式运行项目,在控制台可以看到:

SpringBoot-DispatcherServlet

MyViewResolver

我们可以看到,我们自己编写的视图解析器已经被添加到容器中了,这也验证了我们的猜想。

4.2 格式化器

在文档中,我们还看到有一个叫 Formatter 的东西,见名识意,这是一个格式化器。

那么这个东西该怎么用呢?我们又去找我们的老朋友 WebMvcAutoConfiguration 配置类。因为要使用格式化器,所以我们直接在 MVC 自动配置类中搜索 Formatter,然后就可以看到下列代码:

1
2
3
4
5
6
7
@Bean
@Override
public FormattingConversionService mvcConversionService() {
    WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
    addFormatters(conversionService);
    return conversionService;
}

根据这段代码,我们可以看到日期格式化都是从 mvcProperties 中获取的,我们点击并进入 getDateFormat()

1
2
3
public String getDateFormat() {
    return this.dateFormat;
}

点击 dateFormat

1
2
3
4
5
6
7
8
9
10
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
    // .... 
/**
* Date format to use. For instance, `dd/MM/yyyy`.
*/
private String dateFormat;
 
    // ....
}

可以看到 dateFormat 是在 WebMvcProperties 类中,那么我们是否直接可以在 SpringBoot 配置文件中直接定义日期格式呢?答案是肯定的!😏

1
2
#配置日期格式化
spring.mvc.date-format=yyyy-MM-dd

除了视图解析器和格式化器,其他的配置我们可以按照相同的方法进行设置。

4.3 修改默认配置

SpringBoot 在配置很多组件的时候,先看容器中有没有用户自己配置的(使用@Bean),如果有就用用户配置的,如果没有就用自动配置的。如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!

我们又来看官方文档:

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

翻译一下:如果你想要保留 SpringBoot MVC 的功能,并且希望添加其他的 MVC 功能(比如:拦截器、格式化器、视图控制器等),你可以在你自己编写的类型为 WebMvcConfigurer 的类上添加 @Configuration 注解,但 不添加 @EnableWebMvc

既然如此,那我们可以操作一波了!👊

1
2
3
4
5
6
7
8
9
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器发送/test , 就会跳转到test页面;
        registry.addViewController("/yang").setViewName("test");
    }
}

我们自己编写了一个视图控制器,在浏览器内访问 localhost:8080/test 可以跳转到 test.html 界面。进行测试后,确实可以成功跳转。

这表明了:想要拓展 SpringMVC,且不破坏 SpringBoot 给我们的自动配置,就可以像这么做!


分析一下原理:

继续找我们的老朋友 WebMvcAutoConfiguration 😆,在这个配置类中有一个适配器 WebMvcAutoConfigurationAdapter

1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
    // ...
}

可以看到适配器上有个名为 @Import(EnableWebMvcConfiguration.class) 的注解。我们点击并进入这个注解中的类 EnableWebMvcConfiguration

1
2
3
4
@Configuration(proxyBeanMethods = false)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
    // ....
}

EnableWebMvcConfiguration 类继承了 DelegatingWebMvcConfiguration 类。我们点击并进入父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

    private final WebMvcConfigurerComposite configurers 
        = new WebMvcConfigurerComposite();

    // 获取容器中所有的WebMvcConfigurer
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
    
    // ...
}

在这个类中,搜索刚刚设置的 addViewController:

1
2
3
protected void addViewControllers(ViewControllerRegistry registry) {
    this.configurers.addViewControllers(registry);
}

点击并进入 addViewControllers 方法:

1
2
3
4
5
6
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    for (WebMvcConfigurer delegate : this.delegates) {
        delegate.addViewControllers(registry);
    }
}

在这个方法中,我们可以看到:将所有与 WebMvcConfigurer 相关的配置都一起调用,包括我们自己配置的和 SpringBoot 自动配置的。

不难得出结论:所有的 WebMvcConfiguration 都会被调用,包括 SpringBoot 自动配置的和我们自己配置的。

说了半天,还记得开始挖的坑吗?我为什么要把 without @EnableWebMvc 括起来呢?这就要说到全面接管 SpringMVC 了。👇

4.4 全面接管 SpringMVC

官方文档说不能加 @EnableWebMvc,我偏要加上去试试,加上去后,我们在前面代码的基础上在浏览器内访问 localhost:8080/test。这个时候,我们发现无法正常显示网页了。

这是为啥呢?

主要是因为添加注解 @EnableWebMvc 后,所有的配置将由我们自己接管,而 SpringBoot 自动配置的 MVC 全部失效!


我们在源码中分析一下:

点击并进入 @EnableWebMvc 注解:

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

我们看到了刚交的朋友:DelegatingWebMvcConfiguration。点击并进入它:

1
2
3
4
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    // ...
}

这个类继承了 WebMvcConfigurationSupport,看到这是不是觉得云里雾里,这和我们要讲的 SpringBoot 自动配置的 MVC 失效有啥关系呢?😵

别急,我们最后再拜访一下我们的老朋友 WebMvcAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
@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 })
public class WebMvcAutoConfiguration {
    // ...
}

WebMvcAutoConfiguration 的注释表明,当存在 WebMvcConfigurationSupport 类时,自动配置的 MVC 将不会生效!

😎