封面画师: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将不会生效!!

😎