封面画师:adsuger 封面ID:78096650
0. 前沿
创建一个SpringBoot Web项目的步骤:
创建一个SpringBoot应用,选择我们需要的模块,SpringBoot会自动把我们选择的模块配置好。
在配置文件(properties或yml)中手动进行配置
编写业务代码!完事!✌️
至于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(); 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 <dependency > <groupId > org.webjars</groupId > <artifactId > jquery</artifactId > <version > 3.2.1</version > </dependency >
PS:相关依赖可以在WebJars官网找到:WebJars
我们可以查看导入的jQuery的目录结构:
既然使用webjars就相当于导入了静态资源,那我们启动项目后可以访问到静态资源吗?当然可以,我们可以打开项目,在地址栏输入http://localhost:8080/webjars/jquery/3.2.1/jquery.js
,即可看到如下界面:
1.2 静态资源映射
从源码中,我们可以看到,除了可以使用webjars导入依赖,还可以使用静态资源映射。
我们点击resourceProperties
常量,可以在WebMvcAutoConfigurationAdapter
类中找到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(); } 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目录下”的原因了。
我们不妨测试一手?
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官网
在此,给出一个简单的使用:
1 2 <html lang ="en" xmlns:th ="https://www.thymeleaf.org/" >
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 > <div th:text ="${msg}" > </div > </body > </html >
启动项目,在地址栏输入localhost:8080/test
试试?
我们已经会了最基本的语法了,那Thymeleaf还有其他的语法吗?当然是有的!💪
3.5 Thymeleaf高级语法
高级语法有哪些呢?官方文档走起!
前往文档第10点,查看属性优先级:
我们可以使用这些属性来替换HTML中原生的属性!
那么我们又可以写那些表达式呢?
依旧是官方文档!前往文档第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自动配置参考文档
为了避免链接挂了,我截了个图:
你会发现,我把截图中的某一处用红色矩形圈出,具体原因后面分析!
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)); 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 (); } private static class MyViewResolver implements ViewResolver { @Override public View resolveViewName (String s, Locale locale) throws Exception { return null ; } } }
编写完成后,我们全局搜索DispatcherServlet 中的 doDispatch()
,并给它打个断点:
然后使用Debug模式运行项目,在控制台可以看到:
我们可以看到,我们自己编写的视图解析器已经被添加到容器中了,这也验证了我们的猜想。
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 { 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) { 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 (); @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将不会生效!!
😎