SpringBoot 2.3.4

本文最后更新于:2021年4月7日 晚上

SpringBoot Notes

本文基于SpringBoot 2.3.4.RELEASE

一、SpringBoot简介

官网介绍

  • Create stand-alone Spring applications
  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
  • Provide opinionated ‘starter’ dependencies to simplify your build configuration
  • Automatically configure Spring and 3rd party libraries whenever possible
  • Provide production-ready features such as metrics, health checks, and externalized configuration
  • Absolutely no code generation and no requirement for XML configuration
  • 创建独立的spring应用。
  • 嵌入Tomcat, Jetty Undertow 而且不需要部署他们。
  • 提供的“starters” poms来简化Maven配置
  • 尽可能自动配置spring应用。
  • 提供生产指标,健壮检查和外部化配置
  • 绝对没有代码生成和XML配置要求。

一般把Spring Boot称为搭建程序的 脚手架 或者说是便捷搭建 基于Spring的工程 脚手架。其最主要作用就是帮助开
发人员快速的构建庞大的spring项目,并且尽可能的减少一切xml配置,做到开箱即用,迅速上手,让开发人员关注业务而非配置。

时代背景

SpringBoot基于微服务以及分布式

二、SpringBoot基础

2.1 SpringBoot 特点

2.1.1 依赖管理

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
  • 其父项目, 自动版本仲裁机制
1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
  • 自定义版本号
  • 1、查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。
  • 2、在当前项目里面重写配置,如下自定义mysql 的版本
1
2
3
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
基础依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
  • spring-boot-starter:spring boot场景启动器
  • Spring把每个场景都抽取出来,做成了一个个starts
  • 如我们需要那个场景只需要把对应的启动器来导入进来就可以,不用担心版本。
  • 用什么功能(Web、缓存、kafka等等)导入相关启动器即可
注解区别
  • @Controller@RestController的区别

@RestController注解相当于@ResponseBody@Controller合在一起的作用。

  1. 如果只是使用@RestController注解Controller,则Controller中的方法无法返回jsp页
    面,或者html,配置的视图解析器 InternalResourceViewResolver不起作用,返回
    的内容就是 Return 里的内容。
  2. 如果需要返回到指定页面,则需要用 @Controller 配合视图解析器
    InternalResourceViewResolver才行。 如果需要返回JSON,XML或自定义
    mediaType内容到页面,则需要在对应的方法上加上@ResponseBody注解。

即:

  1. 使用@Controller 注解
  • 在对应的方法上,视图解析器可以解析return 的jsp,html页面,并且跳转到相应页面
  • 若返回json等内容到页面,则需要加@ResponseBody注解
  1. @RestController注解
  • 相当于@Controller+@ResponseBody两个注解的结合。

  • 返回json数据不需要在方法前面加@ResponseBody注解了

  • 使用@RestController这个注解,就不能返回jsp,html页面,视图解析器无法解析

    jsp,html页面

打包插件
1
2
3
4
5
6
7
8
9
<!-- 将应用打包成一个可执行Jar包,直接使用java -jar xxxx的命令来执行 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

2.1.2 自动配置

  • spring-boot-starter-web 为例,进入父工程可发现
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • Tomcat
    • 配置了Tomcat
1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
  • SpringMVC
    • 自动配置MVC常用组件
1
2
3
4
5
6
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.9.RELEASE</version>
<scope>compile</scope>
</dependency>
  • Web
    • 自动配置好Web常见场景,比如字符编码问题
1
2
3
4
5
6
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.9.RELEASE</version>
<scope>compile</scope>
</dependency>
  • 默认包结构

    • 主程序所在包下所有自包的组件都会默认扫描,不需要以前的包扫描配置
    • 想要改变扫描路劲:@SpringBootApplication(scanBasePackages="com.xxx")@ComponentScan 指定扫描路径
  • 各种配置拥有默认值

    • 默认配置最终都是映射到某个类上,如:MultipartProperties
    • 配置文件的值最终会绑定每个类上,这个类会在容器中创建对象
  • 按需加载所有自动配置项

    • 非常多的starter
    • 引入了哪些场景这个场景的自动配置才会开启
    • SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面

2.2 容器功能

2.2.1 组件添加
  • @Configuration
  1. 配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的

  2. 配置类本身也是组件

  3. proxyBeanMethods:代理bean的方法。

    • Full(proxyBeanMethods = true) 「保证每个@Bean方法被调用多少次返回的组件都是单实例的」
    • Lite(proxyBeanMethods = false)「每个@Bean方法被调用多少次返回的组件都是新创建的」‘

    组件依赖必须使用Full模式默认。其他默认是否Lite模式。

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
#####################Configuration使用示例#####################
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

/**
* Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
* @return
*/
@Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
public User user01(){
User zhangsan = new User("zhangsan", 18);
//user组件依赖了Pet组件
zhangsan.setPet(tomcatPet());
return zhangsan;
}

@Bean("tom")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
#####################Configuration测试代码如下#####################
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {

public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}

//3、从容器中获取组件
Pet tom01 = run.getBean("tom", Pet.class);

Pet tom02 = run.getBean("tom", Pet.class);

System.out.println("组件:"+(tom01 == tom02));

//4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892
MyConfig bean = run.getBean(MyConfig.class);
System.out.println(bean);

//如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
//保持组件单实例
User user = bean.user01();
User user1 = bean.user01();
System.out.println(user == user1);

User user01 = run.getBean("user01", User.class);
Pet tom = run.getBean("tom", Pet.class);

System.out.println("用户的宠物:"+(user01.getPet() == tom));
}
}

  • @Bean@Component@Controller@Service@Repository

照常使用即可。


  • @ComponentScan@Import
    • 前面自定义扫描包用到了@ComponentScan,用来扫描所有的组件
    • @Import 给容器中自动创建出这对应类型的组件、默认组件的名字就是全类名

  • @Conditional
    • 条件装配,满足Conditional指定的条件进行注入。
    • Ctrl + H (Intellij IDEA) 打开@Conditional的实现类

@Conditional

  • ConditionalOnMissingClass - 不存在某个类时注入
  • ConditionalOnBean - 存在某个Bean时注入
  • 其余的根据名字可以判断含义

2.2.2 原生配置文件引入
  • @ImportResource
    • @ImportResource(“classpath:beans.xml”)导入Spring的配置文件
    • 用于已经有大量Bean在xml文件中注入了,就使用此方式导入。可用于老项目更新迭代。

2.2.3 配置绑定
  • 1.@ConfigurationProperties + @Component

    • 在实体类上加上注解 @Component 和 @ConfigurationProperties

    • @Component让实体类成为容器组件,才能让底层自动配置

1
2
3
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {}
  • 2.@EnableConfigurationProperties + @ConfigurationProperties
    • 在配置类上加上@EnableConfigurationProperties,开启Car类的绑定功能
    • @ConfigurationProperties加载实体类上,prefix = “前缀”
1
2
3
4
@EnableConfigurationProperties(Car.class)
//1、开启Car配置绑定功能
//2、把这个Car这个组件自动注册到容器中
public class MyConfig {}

2.3 自动配置原理

2.3.1 引导加载自动配置类

  • 在启动类MainApplication头部加上注解 @SpringBootApplication
1
2
3
4
5
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {...}
  • @SpringBootConfiguration
    • 最重要的就是 @Configuration,表明启动类MainApplication就是一个配置类
1
2
@Configuration
public @interface SpringBootConfiguration {...}
  • @ComponentScan

Configures component scanning directives for use with @Configuration classes. Provides support parallel with Spring XML’s context:component-scan element.
Either basePackageClasses or basePackages (or its alias value) may be specified to define specific packages to scan. If specific packages are not defined, scanning will occur from the package of the class that declares this annotation.

  1. 该注解相当于XML文件的<context:component-scan>配置
  2. 如果没有自定义扫描的包,默认扫描声明该注解的类所在包及其子包。所以启动类才应该与控制器包同级。
  • @EnableAutoConfiguration
    • 开启自动配置
1
2
3
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}

先看第一个注解:进入@AutoConfigurationPackage

1
2
3
4
@Import(AutoConfigurationPackages.Registrar.class)//给容器中导入一个组件
public @interface AutoConfigurationPackage {...}
//利用Registrar给容器中导入一系列组件
//将指定的一个包下的所有组件导入进来 -> MainApplication 所在包下。

Registers packages with AutoConfigurationPackages. When no base packages or base package classes are specified, the package of the annotated class is registered.

  1. 向AutoConfigurationPackages注册包
  2. 如果未指定基包或基包类,则会注册带注释类的包。
1
public abstract class AutoConfigurationPackages {...}

Class for storing auto-configuration packages for reference later (e.g. by JPA entity scanner).

  1. 用于存储自动配置包以供以后参考的类(例如,通过JPA实体扫描仪)

再看到AutoConfigurationPackages.Registrar.class

1
2
3
4
5
6
7
8
9
10
11
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}

@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}

register方法打断点,并启动程序。

计算 new PackageImports(metadata).getPackageNames()的值,(Alt + F8 Intellij IDEA)

register断点信息

计算包名

包目录结构

  1. metedata元数据,introspectedClass是启动类。因为该注解是加在启动类上的。
  2. 然后获得启动类所在的根目录报名,最后toArray(new String[0]),封装成一个数组注册。

然后是第二个注解:@Import(AutoConfigurationImportSelector.class)

进入 AutoConfigurationImportSelector,重写了如下方法

1
2
3
4
5
6
7
8
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); 给容器中批量导入一些组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);获取所有需要导入到容器中的配置类。打断点观察:

获取需要的配置类

1
2
3
4
5
6
7
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
  • 通过 SpringFactoriesLoader 工厂类来加载这些配置类。

  • META-INF/spring.factories 这个目录下有相应的配置内容。

1
2
3
4
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
1
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  • 利用工厂加载 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
  • 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
  • spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

springFactories

2.3.2 按需开启自动配置项

虽然默认加载了很多自动配置类,但是这些自动配置类都会加上一些条件的注解。@ConditionalOnClass等 来限定它们的开启。

2.3.3 修改默认配置

  • SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定

  • 生效的配置类就会给容器中装配很多组件

  • 只要容器中有这些组件,相当于这些功能就有了

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改。

2.3.4 实践

2.4 Web开发

2.4.1 SpringMVC自动配置概览

在官网上有 SpringBOOT 整合 SpringMVC 相关的介绍。

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-spring-mvc-auto-configuration

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)

The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

    • 内容协商视图解析器和BeanName视图解析器
  • Support for serving static resources, including support for WebJars (covered later in this document)).

    • 静态资源(包括webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.

    • 自动注册 Converter,GenericConverter,Formatter
  • Support for HttpMessageConverters (covered later in this document).

    • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • Automatic registration of MessageCodesResolver (covered later in this document).

    • 自动注册 MessageCodesResolver (国际化用)
  • Static index.html support.

    • 静态index.html 页支持
  • Custom Favicon support (covered later in this document).

    • 自定义 Favicon
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

    • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

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.

不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则

  • @EnableWebMvc注解原理
1
2
3
...
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {...}

@Import({DelegatingWebMvcConfiguration.class}),所以在@EnableWebMvc配置后,会自动加载DelegatingWebMvcConfiguration.class。而在public class WebMvcAutoConfiguration里面

1
2
3
....
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
public class WebMvcAutoConfiguration {...}

@ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) 这里表示如果WebMvcConfigurationSupport注入时,则WebMvcAutoConfiguration不会被开启。

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport注入DelegatingWebMvcConfiguration 同时也会注入父类WebMvcConfigurationSupport,所以最终的结论是:

  • 注入@EnableWebMvc会让WebMVC自定义配置失效

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

声明 WebMvcRegistrations 改变默认底层组件并且仍然保留Spring Boot MVC自定义设置。

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

使用 @EnableWebMvc + @Configuration + DelegatingWebMvcConfiguration 全面接管SpringMVC

2.4.2 功能分析

2.4.2.1 静态资源访问

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-spring-mvc-static-content

By default, Spring Boot serves static content from a directory called /static (or /public or /resources or /META-INF/resources) in the classpath or from the root of the ServletContext. It uses the ResourceHttpRequestHandler from Spring MVC so that you can modify that behavior by adding your own WebMvcConfigurer and overriding the addResourceHandlers method.

  • 静态资源默认访问路径
    • /META-INF/resources/
    • /resources/
    • /static/
    • /public/
    • 优先级顺序为:META-INF/resources > resources > static > public, 一般来说 public 下放一些公共的资源比如 用户都会访问的 js 文件, static 放一些静态资源,如图片。resources 放一些上传upload文件。
  • 它使用Spring MVC中的ResourceHttpRequestHandler,这样您就可以通过添加自己的WebMvcConfigurer并重写addResourceHandlers方法来修改该行为。

By default, resources are mapped on /**, but you can tune that with the spring.mvc.static-path-pattern property. For instance, relocating all resources to /resources/** can be achieved as follows:


  • 可以修改资源访问的路径(项目resources下面的文件)

You can also customize the static resource locations by using the spring.web.resources.static-locations property (replacing the default values with a list of directory locations). The root Servlet context path, "/", is automatically added as a location as well.

  • 用了 spring.web.resources.static-locations 修改资源文件访问路径,则上面的四个默认的路径就会失效。

  • 默认情况下,资源映射到/** ,但可以使用spring.mvc.static-path-pattern来重新定位到 /resources/**。就是访问资源路径前面加一个前缀。(url)
1
spring.mvc.static-path-pattern=/resources/**
1
2
3
spring:
mvc:
static-path-pattern: "/resources/**"

请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。


  • 静态资源如果是在WebJars这个网站导入的,则下载的目录文件都有同一的路径格式,则从jar文件提供路径为/webjars/**的任何资源。

In addition to the “standard” static resource locations mentioned earlier, a special case is made for Webjars content. Any resources with a path in /webjars/** are served from jar files if they are packaged in the Webjars format.

如导入jquery

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径

2.4.2.2 欢迎页

如下两种情况都会当作静态页处理。1 访问静态页文件 2 Controller进行资源跳转。

  1. 静态资源路径下 index.html
  • 可以配置静态资源路径

  • 但是不可以配置静态资源的访问前缀。否则导致 index.html 不能被默认访问

  • spring.mvc.static-path-pattern ❌❌❌

1
2
3
4
5
6
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效

resources:
static-locations: [classpath:/haha/]
  1. controller能处理/index
2.4.2.3 自定义 Favicon

favicon.ico 放在静态资源目录下即可。

1
2
3
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
2.4.2.4 静态资源配置原理
1
2
3
4
5
6
7
8
@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 {...}
  • ✔ 是一个SERVLET应用
  • ✔ 在WebMVC自动配置类导入之前已经导入Servlet DispatcherServlet WebMvcConfigurer
  • ❌没导入 WebMvcConfigurationSupport
  • ✔ WebMvcAutoConfiguration 执行

  • 然后看到静态内部类 WebMvcAutoConfigurationAdapter
1
2
3
4
...
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {...}

WebMvcProperties.classResourceProperties.class分别绑定前缀属性:spring.mvc, spring.resources

1
2
3
4
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {...}
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {...}
  • 只有一个有参构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
  • 所有参数的值都会从容器中确定
  • ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
  • WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
  • ListableBeanFactory beanFactory Spring的 beanFactory
  • HttpMessageConverters 找到所有的HttpMessageConverters
  • ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器
  • DispatcherServletPath
  • ServletRegistrationBean 给应用注册Servlet、Filter….

  • 内部成员方法addResourceHandlers —-> 资源处理默认规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Override
    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));
    }
    }
  • 先在if (!this.resourceProperties.isAddMappings()) 打断点

进入isAddMappings方法

1
2
3
4
5
6
7
public boolean isAddMappings() {
return this.addMappings;
}
/**
* Whether to enable default resource handling. 是否启用默认资源处理
*/
private boolean addMappings = true; // 默认为true

所以配置上:

1
2
3
spring:
resources:
add-mappings: false 禁用所有静态资源规则

所以可以用这种方式禁用静态资源的访问

  • 中间获取了静态资源的缓存规则,包括缓存时间等
1
2
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
1
2
3
4
5
6
7
8
9
10
public Duration getPeriod() {
return this.period;
}
/**
* Cache period for the resources served by the resource handler. If a duration
* suffix is not specified, seconds will be used. Can be overridden by the
* 'spring.resources.cache.cachecontrol' properties.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration period;

根据注释上的提示,可以,period单位是秒,在配置文件中可以自定义缓存时间

1
2
3
4
5
spring:
resources:
add-mappings: true // 默认为true
cache:
period: 11000 // 单位s
  • 第一个if,如果存在/webjars/**路径,则去 /META-INF/resources/webjars/下找资源,并配置好设置的缓存规则。这种方式就是在WebJars方式导入静态资源。
1
2
3
4
5
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
  • 如果不是webjars方式导入,则从resources目录下找资源

先获取资源路径,默认路径就是 /**

1
2
3
4
5
6
7
8
9
10
String staticPathPattern = this.mvcProperties.getStaticPathPattern();

public String getStaticPathPattern() {
return this.staticPathPattern;
}

/**
* Path pattern used for static resources.
*/
private String staticPathPattern = "/**";

接下来的代码与上面逻辑相同。

1
2
3
4
5
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}

进入this.resourceProperties.getStaticLocations()

1
2
3
4
5
6
7
8
public String[] getStaticLocations() {
return this.staticLocations;
}

private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

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

就是默认的四个静态资源访问路径。


  • 进入静态内部类EnableWebMvcConfiguration
1
2
@Configuration(proxyBeanMethods = false)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {...}
1
2
3
4
5
6
7
8
9
10
@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));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}

welcomePageHandlerMapping这个方法就是控制欢迎页。

进入WelcomePageHandlerMapping有参构造方法

1
2
3
4
5
6
7
8
9
10
11
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Optional<Resource> welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
  • 要用欢迎页功能,路径必须是/**,欢迎页存在,走if

  • 不能存在,走else if:调用Controller /index


  • Favicon

浏览器会自动发送 /favicon.ico 请求获取图标,整个session期间不再获取

2.4.3 请求参数处理

2.4.3.1 请求映射
  • @xxxMapping;

  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作

    • 以前:**/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户
    • 核心Filter:HiddenHttpMethodFilter
      • 用法: 表单method=post,隐藏域 _method=put
      • SpringBoot中手动开启

因为

表单请求方式method只有 GET 和 POST,所以要让过滤器HiddenHttpMethodFilter将请求参数名替换。

四种测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}


@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}

@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}

hiddenHttpMethodFilter来控制method的请求方式。

1
2
3
4
5
6
7
8
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}

public class OrderedHiddenHttpMethodFilter extends HiddenHttpMethodFilter implements OrderedFilter

父类HiddenHttpMethodFilter里的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
}
}
}

filterChain.doFilter((ServletRequest)requestToUse, response);
}

HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);这里实现了method的转换,重写了getMethod().

1
2
3
4
5
6
7
8
9
10
11
12
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;

public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}

public String getMethod() {
return this.method;
}
}
  • 表单提交会带上_method=PUT

  • 请求过来被HiddenHttpMethodFilter拦截

    • 请求是否正常,并且是POST

      • 获取到_method的值。

      • 兼容以下请求;PUT.DELETE.PATCH

      • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。

      • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。

Rest使用客户端工具,

  • 如PostMan直接发送Put、delete等方式请求,无需Filter。
1
2
3
4
5
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能
请求映射原理

DispatcherServlet

SpringMVC功能最终都会经过 org.springframework.web.servlet.DispatcherServlet 的 doDispatch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);

//HandlerMapping:处理器映射。/xxx->>xxxx

handlerMappings

RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。

mappingRegistry

所有的请求映射都在HandlerMapping中。

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;

  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。

    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping

1
2
3
4
5
6
7
8
9
10
11
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
  • 默认使用的是 _method 这个名称的变量,用于携带请求方式
    • 自定义变量名称要自定义 Filter
1
2
3
4
5
6
7
8
9
10
@Configuration(proxyBeanMethods = false)
public class WebConfig{
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}
2.4.3.2 普通参数与基本注解
  • @PathVariable 路径变量
  • @RequestHeader 获取请求头
  • @RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2)
  • @CookieValue 获取Cookie值
  • @RequestAttribute 获取request域属性
  • @RequestBody 获取请求体[POST]
  • @MatrixVariable 矩阵变量
  • @ModelAttribute
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
@RestController
public class ParameterTestController {

// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie){
Map<String,Object> map = new HashMap<>();
// map.put("id",id);
// map.put("name",name);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
map.put("_ga",_ga);
System.out.println(cookie.getName()+"===>"+cookie.getValue());
return map;
}

@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}

//1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
//2、SpringBoot默认是禁用了矩阵变量的功能
// 手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
// removeSemicolonContent(移除分号内容)支持矩阵变量的
//3、矩阵变量必须有url路径变量才能被解析
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}

// /boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();

map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}
}

2.5 数据访问

2.5.1 SQL

1、数据源的自动配置 - HikariDataSource
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

jdbc-starter

数据库驱动?

为什么导入JDBC场景,官方不导入驱动?官方不知道我们接下要操作什么数据库。

数据库版本和驱动版本对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
默认版本:<mysql.version>8.0.22</mysql.version>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- <version>5.1.49</version>-->
</dependency>
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.49</mysql.version>
</properties>
2、分析自动配置
  • DataSourceAutoConfiguration : 数据源的自动配置

    • 修改数据源相关的配置:spring.datasource
    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
    • 底层配置好的连接池是:HikariDataSource
1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration
  • DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置

  • JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud

    • 可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate
    • @Bean@Primary JdbcTemplate;容器中有这个组件
  • JndiDataSourceAutoConfiguration: jndi的自动配置

  • XADataSourceAutoConfiguration: 分布式事务相关的

3、修改配置项
1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
4、测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {

@Autowired
JdbcTemplate jdbcTemplate;

@Test
void contextLoads() {

// jdbcTemplate.queryForObject("select * from account_tbl")
// jdbcTemplate.queryForList("select * from account_tbl",)
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}",aLong);
}
}

鸣谢:

https://spring.io/projects/spring-boot

https://yuque.com/atguigu/springboot

https://blog.csdn.net/weixin_40753536/article/details/81285046


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!