0%

Springboot3学习乱记

常用注解

@Import: 可以在第三方包的类直接导入ioc容器中

1
2
3
4
5
6
@Import(StringUtils.class)
@Configuration //标注该类是一个配置类
@SpringBootConfiguration // 和上面的作用一样,用于人区分,上面做公共配置
public class AppConfig {

}

自动配置原理

场景启动器springboot提供了很多场景启动器,命名规则是spring-boot-starter-xx。如:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

场景启动器的启动器所有场景启动器都会引入-spring-boot-starter

1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>

启动器会帮我们自动加载所有配置,这些配置都在
/org/springframework/boot/spring-boot-autoconfigure/3.1.0/spring-boot-autoconfigure-3.1.0.jar!/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

WebMvcAutoConfiguration

注解

1
2
3
4
5
6
7
8
9
10
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class }) // 在这些配置类注入后再配置
@ConditionalOnWebApplication(type = Type.SERVLET) // 条件: 只有是一个web应用,并且是servlet才生效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) // 有这些bean才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //容器中必须没有WebMvcConfigurationSupport这个类
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {

}

静态资源

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter.addResourceHandlers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(),
"classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
  1. 规则1: 如果访问webjars/webjars/**就去classpath:/META-INF/resources/webjars/寻找资源
1
2
3
4
/**
* Path pattern used for WebJar assets.
*/
private String webjarsPathPattern = "/webjars/**";

可以去webjars的官网找自己想要的资源,如css等。网址: https://www.webjars.org/

  1. 规则2: 所有静态资源需要放到指定的目录下
1
2
3
4
5
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
  1. 规则3: 静态资源的缓存设置
1
2
3
4
5
6
7
8
9
10
11
12
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
Consumer<ResourceHandlerRegistration> customizer) {
if (registry.hasMappingForPattern(pattern)) {
return;
}
ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
customizer.accept(registration);
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
customizeResourceHandlerRegistration(registration);
}

所有静态资源缓存设置都在WebProperties下进行配置
setCachePeriod: 缓存周期,单位s
setCacheControl: http缓存控制
setUseLastModified: 是否使用最后一次修改。即:先访问拿到静态资源的最后一次修改时间,可以来判断是否用本地资源。

缓存实验

相关配置

1
2
3
4
5
6
7
8
9
10
# spring.web 配置 国际化区域信息、静态资源信息、缓存设置、资源映射
# 开启静态资源映射,默认为true
spring.web.resources.add-mappings=true
# 缓存设置cache
# 开启最后一次修改时间。开启后会先拿到静态资源的最后一次修改时间,由此来判断是否需要重新请求或使用本地缓存
spring.web.resources.cache.use-last-modified=true
# 静态资源缓存的时间
spring.web.resources.cache.period=3600
# 更精确缓存策略配置,会覆盖cache
spring.web.resources.cache.cachecontrol.max-age=7200

可以通过请求静态资源,来查看f12控制台。

欢迎页

handlermapping: 处理器映射,根据path找到对应的控制器方法。
欢迎页bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
return createWelcomePageHandlerMapping(applicationContext, mvcConversionService, mvcResourceUrlProvider,
WelcomePageHandlerMapping::new);
}

private <T extends AbstractUrlHandlerMapping> T createWelcomePageHandlerMapping(
ApplicationContext applicationContext, FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider, WelcomePageHandlerMappingFactory<T> factory) {
TemplateAvailabilityProviders templateAvailabilityProviders = new TemplateAvailabilityProviders(
applicationContext);
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
T handlerMapping = factory.create(templateAvailabilityProviders, applicationContext, getIndexHtmlResource(),
staticPathPattern);
handlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}

同样的,this.mvcProperties.getStaticPathPattern() 会在/**下找静态资源,方法getIndexHtmlResource()寻找欢迎页

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
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

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

private Resource getIndexHtmlResource() {
for (String location : this.resourceProperties.getStaticLocations()) {
Resource indexHtml = getIndexHtmlResource(location);
if (indexHtml != null) {
return indexHtml;
}
}
ServletContext servletContext = getServletContext();
if (servletContext != null) {
return getIndexHtmlResource(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
return null;
}

private Resource getIndexHtmlResource(String location) {
return getIndexHtmlResource(this.resourceLoader.getResource(location));
}

private Resource getIndexHtmlResource(Resource location) {
try {
Resource resource = location.createRelative("index.html");
if (resource.exists() && (resource.getURL() != null)) {
return resource;
}
}
catch (Exception ex) {
}
return null;
}

/**
* 也可以使用这种方式,不需要实现WebMvcConfigurer
* @return
*/
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
}
};
}

在指定路径下放一个index.html则项目启动时就会访问。

favicon

同样,放在指定静态资源路径下,则会生效。这是因为chrome浏览器在访问网页时会自动去访问网站的favicon.ico

自定义配置

可以在application.properties中进行配置。

1
2
3
4
5
6
7
# 自定义设置静态资源的读取路径,除了META-INF下生效,其他全部失效
# "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/"
spring.web.resources.static-locations=classpath:/demo/
# 指定webjars的访问路径
spring.mvc.webjars-path-pattern=/mj/**
# 指定静态资源的访问路径前缀
spring.mvc.static-path-pattern=/static/**

也可以用代码的方式配置

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebMvc // 该注解会禁用掉boot的默认配置
public class DemoWebConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
// 自定义
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/demo/")
;
}
}

为什么在容器中放入一个WebMvcConfigurer就会生效?

  1. WebMvcAutoConfiguration中,有一个开启自动配置类EnableWebMvcConfiguration
  2. EnableWebMvcConfiguration继承DelegatingWebMvcConfiguration。并且DelegatingWebMvcConfiguration有一个属性WebMvcConfigurerComposite configurers
  3. 会通过@Autowired自动注入到容器中。会去遍历调用所有WebMvcConfigurer的方法。

路径匹配规则

在Spring5.3,SpringMVC实现controller的路径匹配规则是通过策略AntPathMatcher,但是现在已经支持PathPatternParser。并且可以通过配置来指定。
路径匹配规则:
*: 匹配多个或0字符;
?: 匹配一个或0字符;
/**: 匹配多个目录;
[a-z]+: 匹配a-z多个字符。
SpringBoot中默认的策略是PathPatternParser,与AntPathMatcher不同的是:/**PathPatternParser中匹配只能用在末尾。
可以通过配置来改变使用的路径匹配策略

1
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

如果使用的是PathPatternParser,在路径匹配中存在非末尾的/**,则会在项目启动时报错。
Fix this pattern in your application or switch to the legacy parser implementation with 'spring.mvc.pathmatch.matching-strategy=ant_path_matcher'.
在配置文件中修改策略即可。

一个接口适配多端数据返回(内容协商)

  1. 基于请求头的内容协商
    客户端向服务端发送请求时,在请求头中通过Accept来指定需要接收的响应格式,如Accept:application/textAccept:application/json
  2. 基于请求参数的内容协商 (默认禁用
    例如在发送请求时,在请求参数中添加?format=json来指定需要接收的响应
    开启方式:
1
2
# 开启请求参数的内容协商
spring.mvc.contentnegotiation.favor-parameter=true
  1. spring默认支持json

内容协商原理-HttpMessageConverter

定制HttpMessageConverter来实现多端内容协商
WebMvcConfigurer提供了configureMessageConverters,修改底层的messageConverter

@ResponseBody由底层的HttpMessageConverter处理

所有请求到达服务器,都是由DispatcherServlet拦截来进行路径映射到对应的controller来处理。

  1. 方法:org.springframework.web.servlet.DispatcherServlet.doDispatch
  2. —>> 获取处理器适配器 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  3. —>> 由适配器调用实际的控制处理器 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
  4. —>> 实际处理方法org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod
  5. —>> 处理请求参数解析HandlerMethodArgumentResolverComposite argumentResolvers
  6. —>> 处理返回值解析HandlerMethodReturnValueHandlerComposite returnValueHandlers
  7. —>> 执行真正的方法 org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle
  8. —>> 会调用我们写的controller org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest
  9. —>> 处理返回值 org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue
  10. 通过方法org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.selectHandler来选择满足要求的返回值处理器
    判断方式: 遍历容器中的所有返回值处理器,与调用实际方法的返回值进行判断,找到能够处理返回值的处理器
    而判断条件正式是否含有`@ResponseBody`
    
    RequestResponseBodyMethodProcessor#handleReturnValue 处理返回值;
    实际处理方法: writeWithMessageConverters 拿到要求的响应体类型,确定最后的MediaType后,遍历所有的List<HttpMessageConverter<?>> messageConverters来处理返回值
    由对应的`converter`来处理
    

WebMvcConfigurationSupport

提供了很多默认的配置

内容协商示例-YAML

  1. 依赖
1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
  1. 代码demo
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws JsonProcessingException {
YAMLFactory yamlFactory = new YAMLFactory();
// 禁用文档开始标记
yamlFactory.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
ObjectMapper mapper = new ObjectMapper(yamlFactory);
User user = new User();
user.setAge(12);
user.setId(2L);
user.setName("jack");
String s = mapper.writeValueAsString(user);
System.out.println(s);
}
  1. 在配置文件中新增内容协商的类型
1
2
# 新增内容协商的内容类型
spring.mvc.contentnegotiation.media-types.yaml=application/yaml
  1. 新建自定义的converter
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
public class DemoYamlMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper mapper;

public DemoYamlMessageConverter() {
// 告诉spring该converter支持的是哪一种MediaType,媒体类型
super(new MediaType("application", "yaml", StandardCharsets.UTF_8));
mapper = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));
}

/**
* 支持的对象类型
* @param clazz the class to test for support
* @return
*/
@Override
protected boolean supports(Class clazz) {
return true;
}

/**
* 对请求体参数进行处理,@RequestBody
*
* @param clazz the type of object to return
* @param inputMessage the HTTP input message to read from
* @return
* @throws IOException
* @throws HttpMessageNotReadableException
*/
@Override
protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}

/**
* 处理返回内容
* @param o the object to write to the output message
* @param outputMessage the HTTP output message to write to
* @throws IOException
* @throws HttpMessageNotWritableException
*/
@Override
protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
try (OutputStream os = outputMessage.getBody()) {
this.mapper.writeValue(os, o);
}
}
}

注意: 我们需要告知我们的converter是用来处理什么类型的,我们在构造器中调用父类构造器来进行指定,所指定的类型需要和配置文件中保持一致

  1. 将自定义的converter加入到容器中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
//@EnableWebMvc // 该注解会禁用掉boot的默认配置
public class DemoWebConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
// 自定义
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/demo/")
;
}

/**
* 加入自定义的converter
* @param converters initially an empty list of converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
WebMvcConfigurer.super.configureMessageConverters(converters);
converters.add(new DemoYamlMessageConverter());
}
}

整合thymeleaf

同样的,我们可以找到ThymeleafAutoConfiguration,查看相关的配置参数。
ThymeleafProperties中有两个属性。请求时会去/templates下找.html文件

1
2
3
4
5
6
7
8
/**
* Prefix that gets prepended to view names when building a URL.
*/
private String prefix = DEFAULT_PREFIX;
/**
* Suffix that gets appended to view names when building a URL.
*/
private String suffix = DEFAULT_SUFFIX;

html中引入会有相关thymeleaf提示
<html lang="en" xmlns:th="http://www.thymeleaf.org">

thymeleaf基本语法

  1. th:text: 标签体内文本值渲染
    th:text: 会转义html标签
    th:utext: 不会转义,会显示html标签,以及各种设置好的样式,如服务端在Model中放了<span style='color:red'>Jack</span>则会显示红色字体。而th:text则会转义。
  2. th:属性: 标签体内属性渲染
    th可以在任意html标签,这样可以做到动态替换
1
<img th:src="${imgUrl}" style="width:250px">
  1. th:attr: 标签体内任意值渲染
    任意属性指定
1
2
<!-- 任意属性指定 -->
<img th:attr="src=${imgUrl}, style=${myStyle}">
  1. th:if th:each… 条件判断、遍历等…

表达式

  1. ${} 取各种属性值
  2. @{} 专门用来取路径,会动态的根据项目的跟路径进行路径的变化
1
<img th:src="@{${imgUrl}}" />

热部署

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

thymeleaf的一些配置

1
2
3
4
5
6
# 默认页面路径
spring.thymeleaf.prefix=classpath:/templates/
# 页面后缀
spring.thymeleaf.suffix=.html
# 开发时关闭,生产开启,是否使用模版缓存
spring.thymeleaf.cache=true

SpringBoot3核心原理

1. 事件与监听器

生命周期监听
监听器:

自定义SpringApplicationRunListener

  1. 编写SpringApplicationRunListener 实现类
  2. META-INF/spring.factories中配置org.springframework.boot.SpringApplicationRunListener=xxxListener,还可以指定一个有参构造器,
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
package com.turbo.springboot3web.config;

import org.springframework.boot.*;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

import java.time.Duration;

/**
* 引导
* 先获取BootstrapContext-引导启动的上下文: {@link SpringApplication#createBootstrapContext()}
* starting: 获取所有SpringApplicationRunListener - {@link org.springframework.boot.SpringApplication#getRunListeners(java.lang.String[])}
* environmentPrepared-环境准备:{@link SpringApplication#prepareEnvironment(SpringApplicationRunListeners, DefaultBootstrapContext, ApplicationArguments)}, 在环境准备好后辉调用`listeners.environmentPrepared(bootstrapContext, environment);`
* contextPrepared ioc容器准备好,即创建了ioc容器: {@link org.springframework.boot.SpringApplication.prepareContext}
* listeners.contextPrepared(context);
* bootstrapContext.close(context); ioc容器准备后辉关闭引导启动器
* contextLoaded 加载完ioc容器后包括启动主类, 会调用
* started 刷新完ioc容器后会调用,即所有bean都被加载完成
* ready 如果容器运行,则进入就绪状态
*/
public class CustomRunListener implements SpringApplicationRunListener {

@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("======正在启动====");
}

@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("=======环境准备完成======");
}

@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("=======IOC上下文准备完成======");
}

@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("=======容器加载完成======");
}

@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("=======启动完成======");
}

@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("=======准备就绪======");
}

@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("=======应用启动失败");
}
}

SpringApplcationRunListener可以感知Spring的全生命周期,并且可以对容器进行操作

ApplicationListener

也可以感知全阶段,基于事件监听

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

void onApplicationEvent(E event);

static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
return event -> consumer.accept(event.getPayload());
}

}
ApplicationRunner

感知特定阶段: 感知应用就绪(ready)

CommandLineRunner

感知特定阶段: 感知应用就绪(ready)

BootstrapRegistryInitializer

感知特定阶段: 感知引导启动器(starting)
关键字SpringFactories,方法中包含这个关键字,都是从spring.factories配置文件里面去获取, 所以如果自定义,需要在spring.factories中配置
如下代码: this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));

1
2
3
4
5
6
7
8
9
10
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
ApplicationContextInitializer

感知特定阶段: 感知容器准备(contextPrepared)

基于事件驱动开发(ApplicationListener

  1. 事件发布者:
    事件发布者可以发送事件,实现接口ApplicationEventPublisherAware, 实现方法注入事件发布者
  2. 事件监听者
    实现自己的事件? extends ApplicationEvent, 通过两种方式:
    1. 实现ApplitionListener
    2. 使用注解@EventListener
-------------本文结束感谢您的阅读-------------