Spring Boot Web开发

Spring Boot Web开发

1、使用Spring Boot:

  • 创建Spring Boot应用,选中我们需要的模块
  • Spring Boot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来
  • 自己编写业务代码

回顾自动配置原理

这个场景Spring Boot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?等等

  • xxxxAutoConfiguration:帮我们给容器中自动配置组件
  • xxxxProperties:配置类来封装配置文件的内容

2、Spring Boot对静态资源的映射规则

1
2
3
4
5
6
7
8
@ConfigurationProperties(
prefix = "spring.resources",
ignoreUnknownFields = false
)
public class ResourceProperties implements ResourceLoaderAware {
// 可以设置和静态资源有关的参数,比如缓存时间
...
}

2.1 依赖的方式

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
// WebMvcAutoConfiguration.class文件
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Integer cachePeriod = this.resourceProperties.getCachePeriod();
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(
new String[]{"/webjars/**"}).addResourceLocations(
new String[]{"classpath:/META-INF/resources/webjars/"}
).setCachePeriod(cachePeriod)
);
}

String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(
new String[]{staticPathPattern}).addResourceLocations(
this.resourceProperties.getStaticLocations()
).setCachePeriod(cachePeriod)
);
}

}
}
  • 所有/webjars/**,都去classpath:/META-INF/resources/webjars/找资源

    webjars:以jar包的方式引入静态资源

webjars官网

webjars-jquery

访问方式:http://localhost:8088/webjars/jquery/3.5.1/jquery.js

1
2
3
4
5
6
<!-- 引入jquery-webjars -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>

2.2 “/**”访问当前项目的任何资源(静态资源的文件夹)

继续读addResourceHandlers方法,如果处理不了,就去静态staticPathPattern路径去找

1
2
3
4
5
6
7
8
9
10
11
12
13
public WebMvcProperties() {
this.localeResolver = WebMvcProperties.LocaleResolver.ACCEPT_HEADER;
this.dispatchTraceRequest = false;
this.dispatchOptionsRequest = true;
this.ignoreDefaultModelOnRedirect = true;
this.throwExceptionIfNoHandlerFound = false;
this.logResolvedException = false;
this.mediaTypes = new LinkedHashMap();
this.staticPathPattern = "/**";
this.async = new WebMvcProperties.Async();
this.servlet = new WebMvcProperties.Servlet();
this.view = new WebMvcProperties.View();
}

它添加了个位置addResourceLocations,这个里面的resourceProperties有个getStaticLocations()方法,点进去,有个常量RESOURCE_LOCATIONS

1
2
3
4
5
6
7
8
static {
RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length +
SERVLET_RESOURCE_LOCATIONS.length];
System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0,
SERVLET_RESOURCE_LOCATIONS.length);
System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS,
SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length);
}

就找到了以下文件夹

1
2
3
4
5
6
7
8
private static final String[] SERVLET_RESOURCE_LOCATIONS = new String[]{"/"}; // 当前项目的根路径
// 和
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};

src/mian/javasrc/main/resources都是类路径

如果要访问localhost:8088/*.js资源,没有人为处理,那么会自动去上面这些路径,静态资源文件夹里找这些文件

访问方式:http://localhost:8088/asserts/img/childhood_dreams.jpg

static

2.3 欢迎页设置,静态资源文件夹下的所有index.html页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// WebMvcAutoConfiguration.class文件
// 配置欢迎页映射
@Bean
public WebMvcAutoConfiguration.WelcomePageHandlerMapping welcomePageHandlerMapping
(ResourceProperties resourceProperties) {
return new WebMvcAutoConfiguration.WelcomePageHandlerMapping(
resourceProperties.getWelcomePage(), this.mvcProperties.getStaticPathPattern()
);
}

private void customizeResourceHandlerRegistration(ResourceHandlerRegistration registration) {
if (this.resourceHandlerRegistrationCustomizer != null) {
this.resourceHandlerRegistrationCustomizer.customize(registration);
}

}

Spring最顶层的组件,保存每个请求谁来处理

点进getWelcomePage()方法

this.mvcProperties.getStaticPathPattern()还被谁映射,点进去看源码就是/**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Resource getWelcomePage() {
String[] var1 = this.getStaticWelcomePageLocations();
int var2 = var1.length;

for(int var3 = 0; var3 < var2; ++var3) {
String location = var1[var3];
Resource resource = this.resourceLoader.getResource(location);

try {
if (resource.exists()) {
resource.getURL();
return resource;
}
} catch (Exception var7) {
}
}

return null;
}

需要遍历,说明欢迎页还挺多的

点进getStaticWelcomePageLocations()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private String[] getStaticWelcomePageLocations() {
String[] result = new String[this.staticLocations.length];

for(int i = 0; i < result.length; ++i) {
String location = this.staticLocations[i];
if (!location.endsWith("/")) {
location = location + "/";
}

result[i] = location + "index.html";
}

return result;
}

还是这个staticLocations静态文件夹

静态文件夹路径都拼接上index.html

访问方式:http://localhost:8088/ 就会找index页面

2.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
// WebMvcAutoConfiguration.class文件
// 配置喜欢的图标
@Configuration
@ConditionalOnProperty(
value = {"spring.mvc.favicon.enabled"},
matchIfMissing = true
)
public static class FaviconConfiguration {
private final ResourceProperties resourceProperties;

public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties;
}

@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(-2147483647);
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
this.faviconRequestHandler()));
return mapping;
}

@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler.setLocations(this.resourceProperties.getFaviconLocations());
return requestHandler;
}
}

所有的**/favicon.ico都是在静态文件夹下找

2.5 自定义路径

1
spring.resources.static-locations=classpath:/hello/,classpath:initializr/

多路径用都好隔开,一旦启用自定义路径,那么默认的静态资源路径就不能使用了

3、模板引擎

JSP、Velocity、Freemarker、Thymeleaf

template-engine

Spring Boot推荐的Thymeleaf:语法更简单,功能更强大

3.1 引入Thymeleaf,引入starter:

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

查看引入的库,发现引入的2.1.6版本的,太低了

Thymeleaf的版本发布

Spring官网Thymeleaf 3的使用

切换版本:

1
2
3
4
5
6
7
8
<properties>
...
<!-- Thymeleaf主程序 -->
<thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
<!-- Thymeleaf布局支持程序 Thymeleaf3要求layout2以上版本 -->
<thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
<!-- Thymeleaf2和layout1适配 -->
</properties>

properties里面的会属性覆盖Spring Boot默认的版本号

thymeleaf-layout-dialect的版本发布

3.2 Thymeleaf使用及语法

还是在spring-boot-autoconfigure自动配置包里:添加组件

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
67
68
69
70
71
72
73
74
@Configuration
@ConditionalOnClass(
name = {"org.thymeleaf.templatemode.TemplateMode"}
)
static class Thymeleaf3Configuration {
Thymeleaf3Configuration() {
}

@Configuration
@ConditionalOnClass({Servlet.class})
@ConditionalOnWebApplication
static class Thymeleaf3ViewResolverConfiguration extends
AbstractThymeleafViewResolverConfiguration {

Thymeleaf3ViewResolverConfiguration(ThymeleafProperties properties,
SpringTemplateEngine templateEngine) {

super(properties, templateEngine);
}

protected void configureTemplateEngine(ThymeleafViewResolver resolver,
SpringTemplateEngine templateEngine) {

Method setTemplateEngine;
try {
setTemplateEngine = ReflectionUtils.findMethod(
resolver.getClass(),
"setTemplateEngine",
new Class[]{Class.forName(
"org.thymeleaf.ITemplateEngine",
true,
resolver.getClass().getClassLoader()
)}
);
} catch (ClassNotFoundException var5) {
throw new IllegalStateException(var5);
}

ReflectionUtils.invokeMethod(setTemplateEngine, resolver, new Object[]{templateEngine});
}
}

@Configuration
@ConditionalOnMissingBean(
name = {"defaultTemplateResolver"}
)
static class DefaultTemplateResolverConfiguration extends
AbstractTemplateResolverConfiguration {

DefaultTemplateResolverConfiguration(ThymeleafProperties properties,
ApplicationContext applicationContext) {

super(properties, applicationContext);
}

@Bean
public SpringResourceTemplateResolver defaultTemplateResolver() {

SpringResourceTemplateResolver resolver = super.defaultTemplateResolver();
Method setCheckExistence = ReflectionUtils.findMethod(
resolver.getClass(),
"setCheckExistence",
new Class[]{Boolean.TYPE}
);

ReflectionUtils.invokeMethod(
setCheckExistence,
resolver,
new Object[]{this.getProperties().isCheckTemplate()}
);
return resolver;
}
}
}

因为我们引入的是3+版本,所以只有3+版本生效;

配置了哪些属性?在文件ThymeleafProperties中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");
private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");
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 = "HTML5";
private Charset encoding;
private MimeType contentType;
private boolean cache;
private Integer templateResolverOrder;
private String[] viewNames;
private String[] excludedViewNames;
private boolean enabled;
}

只要我们把HTML页面放在classpath:/templates/,Thymeleaf就能自动渲染

Thymeleaf语法还是参照官方网官方文档

3.3 代码示例

  • 导入Thymeleaf的名称空间
1
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  • 使用Thymeleaf语法
1
<div th:text="${text}">这里是div信息</div>

3.4 语法规则

th:text:改变当前元素里面的文本内容;

th:任意html属性;替换原生属性的值,例:th:class覆盖class

Feature Detail Attribute
Fragment inclusion 片段包含:jsp:include th:insert
th:replace
Fragment iteration 遍历:c:forEach th:each
Conditional evaluation 条件判断:c:if th:if
th:unless
th:switch
th:case
Local variable definition 声明变量:c:set th:object
th:with
General attribute modification 任意属性修改支持prepend, append th:attr
th:attrprepend
th:attrappend
Specific attribute modification 修改制定属性默认值 th:value
th:href
th:src
Text (tag body modification) 修改标签体内容 th:text(转义特殊字符)
th:utext(不转义特殊字符)
Fragment specification 声明片段 th:fragment
Fragment removal th:remove

3.5 表达式

  • Simple expressions: (表达式语法)

    • Variable Expressions: ${…}: 获取变量值; OGNL;
    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
     1) 获取对象的属性、调用方法
    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.

    ${session.foo} // Retrieves the session atttribute 'foo'
    # 附录都有演示
    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).
    • Selection Variable Expressions: *{…}: 选择表达式, 和${}在功能上是一样的
    1
    2
    3
    4
    5
    6
    补充: 配合th:object="${session.user}"进行使用
    <div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
    </div>
    • Message Expressions: #{…}: 获取国际化内容
    • Link URL Expressions: @{…}: 定义URL
    1
    @{/order/process(execId=${execId},execType='FAST')}
    • Fragment Expressions: ~{…}: 片段引用表达式
    1
    2
    # 插入片段文档
    <div th:insert="~{commons :: main}">...</div>
  • Literals(字面量)

1
2
3
4
5
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:(文本操作)
1
2
String concatenation: +
Literal substitutions: |The name is ${name}|
  • Arithmetic operations:(数学运算)
1
2
Binary operators: + , - , * , / , %
Minus sign (unary operator): -
  • Boolean operations:(布尔运算)
1
2
Binary operators: and , or
Boolean negation (unary operator): ! , not
  • Comparisons and equality:(比较运算)
1
2
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
  • Conditional operators:(条件运算, 也支持3元运算符)
1
2
3
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
  • Special tokens:(特殊符号)
1
No-Operation: _

4、SpringMVC自动配置

可以参阅官方文档Developing web applications

4.1 Spring Boot为Spring MVC提供了自动配置,可与大多数应用程序完美配合。

自动配置会在Spring的默认设置之上添加以下功能:

  • 包含ContentNegotiatingViewResolverBeanNameViewResolver Bean

    • 自动配置了ViewResolver(视图解析器:根据方法的返回值得到视图对象(就是View对象),视图对象决定如何渲染(转发?重定向?等))
    • ContentNegotiatingViewResolver:组合所有的视图解析器的
    • 如何定制:我们可以自己给容器中添加一个视图解析器,自动将其组合进来

    • Ctrl+N输入DispatchServlet.class在它的doDispatch方法前面设置断点,Debug运行,浏览器访问页面,查看控制台。看DispatchServlet里用到的视图解析器是什么?得到如图:

    viewResolver

  • 支持提供静态资源,包括对WebJars的支持(请参见下文)。

  • 自动注册ConverterGenericConverterFormatter bean。

    • Converter:转换器,类型转换使用。例如前端文本转后台Integer
    • Formatter:格式化器,2020.08.13===Date
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 配置文件中配置日期格式化的规则
    @Bean
    @ConditionalOnProperty(
    prefix = "spring.mvc",
    name = {"date-format"}
    )
    public Formatter<Date> dateFormatter() {
    return new DateFormatter(this.mvcProperties.getDateFormat()); // 日期格式化组件
    }
    • 自己添加的格式化器,我们只需要放在容器中即可
  • 支持HttpMessageConverters(请参见下文)。

    • HttpMessageConverter:SpringMVC中用来转换HTTP请求和响应的;例:User对象以JSON形式写出
    • HttpMessageConverters是从容器中确定的,获取所有的HttpMessageConverter
    • 自己给容器中添加HttpMessageConverter,只需将自己的组件注册在容器中(@Bean, @Component)
  • 自动注册MessageCodesResolver(请参见下文)。

    • 定义错误代码生成规则(例:JSR303校验时)
  • 静态index.html支持。

  • 自定义Favicon支持(请参阅下文)。

  • 自动使用ConfigurableWebBindingInitializer bean(请参见下文)。

    • 我们可以配置一个ConfigurableWebBindingInitializer来替换默认的;(添加到容器中)
    • 初始化WebDataBinder(web数据绑定器);
    • web数据绑定器的功能:请求数据=====绑定到JavaBean中

org.springframework.boot.autoconfigure.web:web的所有自动配置场景。

如果您想保留Spring Boot MVC功能,而只想添加其他MVC配置(拦截器,格式化程序,视图控制器等),则可以添加自己的类型为WebMvcConfigurerAdapter@Configuration类,但无需@EnableWebMvc。如果希望提供RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver的自定义实例,则可以声明一个提供此类组件的WebMvcRegistrationsAdapter实例。

如果要完全控制Spring MVC,则可以添加用@EnableWebMvc注释的自己的@Configuration

4.2 扩展SpringMVC

1
2
3
4
5
6
7
<mvc:view-controller path="/hello" view-name="success"/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/hello"/>
<bean></bean>
</mvc:interceptor>
</mvc:interceptors>

要实现以上Spring配置文件的内容,编写一个配置类(@Configuration),是WebMvcConfigurerAdapter类型;不能标注@EnableWebMvc

1
2
3
4
5
6
7
8
9
10
11
// 使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能
@Configuration
public class SpringMVCConfig extends WebMvcConfigurerAdapter {

@Override
public void addViewControllers(ViewControllerRegistry registry) {
// super.addViewControllers(registry);
//浏览器发送 /addViewController 请求来到 thymeleaf
registry.addViewController("/addViewController").setViewName("thymeleaf");
}
}

既保留了所有的自动配置,也能用我们扩展的配置;

原理:

  • WebMvcAutoConfiguration是SpringMVC的自动配置类
  • 在做其他自动配置时会导入;@Import(EnableWebMvcConfiguration.class)
1
2
3
4
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
...
}

点进其父类DelegatingWebMvcConfiguration查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

public DelegatingWebMvcConfiguration() {
}

//自动装配, 从容器中获取所有的WebMvcConfigurer
@Autowired(
required = false
)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
// 一个参考实现; 将所有的WebMvcConfigurer相关配置都来一起调用;
}

}
}

WebMvcConfigurerComposite类中的一个实现

1
2
3
4
5
6
7
8
9
public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();

while(var2.hasNext()) {
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry);
}

}
  • 容器中所有的WebMvcConfigurer都会一起起作用(包括我们自己写的);
  • 我们的配置类也会被调用;
    • 效果:SpringMVC的自动配置和我们的扩展配置都会起作用;

4.3 全面接管SpringMVC

SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己配置;所有的SpringMVC的自动配置都失效了(静态资源也不能访问了)

我们需要在配置类中添加@EnableWebMvc即可;

原理:为什么@EnableWebMvc自动配置就失效了?

  • @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
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
...
}
  • 在找到WebMvcAutoConfiguration类,查看签名
1
2
3
4
5
6
7
8
9
10
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurerAdapter.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
...
}

@ConditionalOnMissingBean():容器中没有这个组件的时候,这个自动配置类才生效

  • @EnableWebMvc将WebMvcConfigurationSupport组件导入进来;
  • 导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能。

5、如何修改Spring Boot的默认配置

模式:

  • Spring Boot在自动配置很多组件时,先看容器中有没有用户自己配置的(@Bean, @Component),如果有就用用户配置的,如果没有才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默认的组合起来;
  • 在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置
  • 在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置

评论