SpringBoot学习笔记
学习目标:
因为很早就学习过了SpringBoot,这次学习要更加深入源码与学习原理。同时学习Spring5新推荐的模式响应式编程。(持续更新)
笔记尽可能的做到细致,个人学习笔记,难免理解有误。如果与你理解有冲突,还是以你为准。
1、SpringBoot底层注解详解
1.@Configuration
以往我们使用Spring框架的时候,配置文件都写在一个Spring.xml中,写了很多之后。这个xml文件的内容越来越多,SpringBoot默认支持自动配置。
我们只需要将额外需要配置的信息写成配置类的形式来进行配置即可,这种形式在之后的开发中非常常见。
比如配置完Mybatis-Plus需要导入插件配置Mybatis-Config、比如各种中间件需要自定义Json转换形式的时候的配置。
以前使用xml的形式进行配置(必须写get、set方法)
<!-- xml的形式给容器中加入一个组件-->
<bean id="user01" class="com.wlw.myself.bean.User">
<property name="age" value="18"></property>
<property name="name" value="zhangsan"></property>
</bean>
SpirngBoot推荐我们使用配置类,使用配置注解,自动识别为配置。
@Configuration //告诉SpringBoot这是一个配置类 == 等于以前Spring框架中的xml配置文件
public class MyConfig {
@Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
public User user(){
return new User();
}
}
启动程序发现我们配置类的组件已经被加入到了ioc容器中
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
//1、这就是我们spring的ioc容器
ConfigurableApplicationContext ioc = SpringApplication.run(MainApplication.class, args);
//2、查询容器里面的组件
String[] beanDefinitionNames = ioc.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println(beanDefinitionName);
}
}
}
控制台输出我们给容器中加入的组件,本身这个配置类、这个Bean都会被加到容器中
SpringBoot2.0支持我们对配置类加上Spring的CgLiib增强代理(继承的方法实现动态代理)检查,属性proxyBeanMethods指定我们的配置类是否创建代理对象。设置成true每次调用配置类中的组件,SpringBoot就会给你容器中的单实例对象,设置成false,则每次调用创建一个新的对象。
所以我们使用的时候要注意,如果你希望这个Bean是单实例的并且别的组件可以进行依赖,那就设置成true,如果没有这个需求,那就设置成false,因为这会加快SpringBoot的启动速度,让每次在调用容器类型的对象的时候不再去容器中进行检查。
设置成true,此时拿到的是增强版的CGLIB代理对象
总结
• 最佳实战
• 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
• 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
@Configuration总结
1、以后我们需要给容器中加配置、或者修改默认配置bean使用@Configuration + @Bean的形式。
2、@Configuration还是使用Spring的ioc思想,默认使用单实例bean。
3、配置类本身也是一个组件,使用FULL模式的时候,保存的是代理对象。
4、我们在有需要使用组件依赖的时候使用FULL模式,让SprinBoot检查维持单实例bean,不需要的时候使用Lite的轻量模式,这样会让SpingBoot启动更快,无需在调用的时候都去检查容器Bean。
5、@Bean、@Component、@Controller、@Service、@Repository 这些SpringMvc的注解SpringBoot也支持,都可以给容器中加入组件。
2、@Impor这个组件可以调用某个类的无参构造器,创建一个以全类名为名的组件并加入到ioc容器中,本身是一个数组支持写入多个bean。
测试@Import的组件
System.out.println("-----------------测试@Import导入的组件-----");
String[] beans = ioc.getBeanNamesForType(User.class);
for (String s : beans) {
System.out.println(s);
}
没有无参构造器会报错
加入无参构造器创建对象成功,组件名为对象名
总结:
1、@Import支持导入组件
2、@Import调用构造器创建一个组件并且组件名是它的全类名
3、@Conditional 条件装配。
SpringBoot使用@Conditional 来对你需要的场景进行条件装配只有你满足了这个条件,这个组件才会生效。
SpringBoot的自动装配功能正是利用这个注解,当你导入了某些依赖的时候,就会触发SpringBoot的条件装配,从而帮助我们自动配置。
Conditional 的各种子接口
4、@ImportResource支持我们导入原先xml的配置文件,只要写好配置地址即可。
如果你的项目还有使用xml配置,想要使用SpringBoot也很简单,使用这个注解可以将xml配置一起迁移给容器管理。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- xml的形式给容器中加入一个组件-->
<bean id="user01" class="com.wlw.myself.bean.User">
<property name="age" value="18"></property>
<property name="name" value="zhangsan"></property>
</bean>
、、
</beans>
xml配置迁移生效
xml中配置的bean也被配置到我们的容器中了。
总结
1、@ImportResource帮我们重构原来的配置文件,不需要所有配置都搬家,导入xml直接将原先xml配置变成SpringBoot统一管理。
5、配置绑定@ConfigurationProperties 与@EnableConfigurationProperties
这两个注解都是用来绑定配置,原先来配置绑定非常麻烦,需要使用java代码解析配置文件,然后使用。
SpringBoot支持使用1、@ConfigurationProperties 2、@EnableConfigurationProperties 3、@Value 三个注解读取配置文件中的配置。
yml中配置我们自定义的属性
mycar:
brand: BYD
price: 100000
将这个bean加入到容器后面使用@Autowired就可以拿到这个bean
/**
* 只有在容器中的组件,才会拥有SpringBoot提供的强大功能
*/
@NoArgsConstructor
@ToString
@Data
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
private String brand;
private Integer price;
}
@Autowired
Car car;
@GetMapping(value = "/mycar")
public Car car(){
return car;
}
成功获取配置文件中的数据
也可以在配置类中使用@EnableConfigurationProperties(Car.class)的形式来将这个类注册到容器中,效果也是一样的。这可以用于我们需要第三方的组件,但是没办法在源代码中使用@Component注解。这个注解必须配合@ConfigurationProperties一起使用。
最后也可以通过@Value + 表达式的形式获取yml中的配置,效果也是一样的
@NoArgsConstructor
@ToString
@Data
@Component
public class Car {
@Value(value = "${mycar.brand}")
private String brand;
@Value(value = "${mycar.price}")
private Integer price;
}
总结:
1、可以通过@ConfigurationProperties获取yml配置。
2、通过配置类的EnableConfigurationProperties +组件的@ConfigurationProperties会自动将bean加入容器。
3、通过@Value加表达式的形式直接从yml中读取配置。
2、自动配置源码分析
SpringBoot最开始的注解时@SpringBootApplication,我们从这个注解开始分析。点开注解发现这个是一个合成注解。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
1、@SpringBootConfiguration
点开发现是合成了@Configuration,所以说本身主启动类也是一个配置类
2、@ComponentScan,组件扫描,就是Spring底层扫描了哪些组件。
3、@EnableAutoConfiguration,着重讨论讨论这个注解,这个注解就是帮助我们完成自动配置。
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
1、@AutoConfigurationPackage
这个注解作用就将我们主程序同级及以下的包纳入ioc容器进行管理。这个注解点进去
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
使用@Import注解导入了一个Registrar的对象到容器中
//利用这个静态内部类给容器中导入了很多组件
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));
}
}
debug起来发现,在SpringBoot主程序启动的时候会将主程序包下的所有bean都注册到ioc容器中。发现在注册的时候会将我们主程序类的所有包扫描纳入ioc容器尽心管理,所以我们必须将文件放在主程序或者主程序的子包下,当然你也可以自己进行设置。
2、@Import(AutoConfigurationImportSelector.class)
导入这些组件,具体需要导入使用一个Sring的数组来存储起来了。所以我们着重看getAutoConfigurationEntry()这个方法
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
//注解元数据是否是为null
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//获取注解元数据中的所有候选者配置类,debug会发现这时有127个
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);
}
候选者的配置类怎么获取的?点进去,使用Spring的工厂加载器加载一些配置
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;
}
再点进去怎么通过工厂模式获取配置的?发现是这个类帮我们去加载Meta-Inf下的properties的配置文件。
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
//通过类装载器来获取AutoConfig包下的MEAT-INF下的properties问你件,
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
//最终将这些配置变成一个map存储
LinkedMultiValueMap result = new LinkedMultiValueMap();
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
127个候选者配置
3、这么多候选者配置,有些jar包我们根本没有导入?通过bebug发现SpringBoot会按需加载这些候选者配置。
通过rabbitmq的自动配置,很好的看出来SpringBoot大量使用@Conditionalxxx这个注解,来动态的去加载这些配置,如果我们没有导入amqp中的rabbitmq模板类,和信道channel类,这些自动配置就不生效。如果你引入依赖starter才会进行自动装配。
小节:
1、SpringBoot启动的时候会去扫描boot.AutoConfigure写的spring.properties文件加载127个候选配置类。
2、SpringBoot通过条件装配注解实现,按需加载配置,根据你导入的依赖/场景启动器。
4、自动配置流程
从我们以前最经常使用的MVC中的DispatchServlet观察SpringBoot的设计模式
//不开启增强代理模式,这个类每次依赖都创建不进行检查
@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
//如果有ServletRegistration这个类,这个内部配置类才会生效
@ConditionalOnClass(ServletRegistration.class)
//开启配置绑定功能,将配置类加入到容器中,并获取配置类中的属性
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {
//注册这个bean的名字为指定的名字
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
//方法签名处,如果指定的类,默认SpringBoot会帮我们去容器中获取这个类型的组件
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
//创建DispatcherServlet ,设置这些属性,通过配置类
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
/**
这个组件官方的注释意思是如果用户创建了一个不规范的组件,我们将这个组件变成规范的
ConditionalOnBean 有这个mvc九大组件中的MultipartResolver才会进行注册 ConditionalOnMissingBean并且容器中的组件名称不是规范的
返回这个容器中的组件
*/
@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
}
这些配置又会和yml中的前缀绑定,但很多有默认值,默认值就是SpringBoot的默认配置,我们可以通过修改配置来更改配置
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
/**
* Formatting strategy for message codes. For instance, `PREFIX_ERROR_CODE`.
*/
private DefaultMessageCodesResolver.Format messageCodesResolverFormat;
/**
* Locale to use. By default, this locale is overridden by the "Accept-Language"
* header.
*/
private Locale locale;
总结一下SpringBoot的自动配置设计思想
1、自动配置类XXXAutoConfiguration绑定了配置类XXXproperties。XXXproperties绑定yml文件。
2、根据条件装配加载配置类,配置类会给容器加入很多组件
3、有这些组件就开启这些功能,不规范的配置,SpringBoot会帮你规范
4、SpringBoot首先支持用户定制化配置,然后才是自动配置
5、想要定制化配置
1、写配置类然后@Bean只需要参考boot的自动配置类
2、通过修改yml的形式动态加载配置
5、SpringBoot开发总结
- 引入场景依赖,starter。SpringBoot的各种场景启动器
- 查看给我们自动配置了些什么
1、自己分析,引入场景对应的自动配置一般都生效了,打开自动配置报告,改日志级别为bebug
2、配置文件中debug=true开启自动配置报告。Negative(不生效)\Positive(生效) - 是否需要修改
1、参照文档修改配置项 官方可配置的properties详解
2、自己分析。看源码xxxxProperties绑定了配置文件的哪些,哪些设置了默认配置 - 自定义加入或者替换组件 @Bean @Component
- 自定义器 XXXXXCustomizer;(后面再深入)
。。。
3、常用开发插件
1、Lombok简化开发
1、先去idea的插件市场上安装lombok插件
2、导入lombok依赖,SpringBoot已经帮我们配置好版本号了,直接使用即可。
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok
lombok简化bean的开发
-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3、使用注解@Data(get,set)、@ToString、@Slf4
j等简化开发,帮我们在编译加入而不是在源代码中写出来。
@NoArgsConstructor
@ToString
@Data
public class Car {
2、热部署 dev-tools
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
我们平常在开发期间如果修改java源代码、配置文件、前端代码不想重新启动就生效,就可以使用dev-tools工具。修改完成之后重新build一下项目,dev-tools会帮我们完成修改重新启动。(dev-tools后端代码只是帮我们自动重启,不是重新加载,前端页面帮我们重新加载。如果需要完全重新加载需要到插件市场上付费JRebel)
2、SpringBoot的初始化向导 Spring initailizr
idea中我们可以专门创建一个自带SpringBoot环境的工程。网络不好的情况,可以将url给为阿里的云。
https://start.aliyun.com
方便的地方就是创建完成自动springboot环境,并且可以搜到springboot的所有场景启动器,比如常用的openfeign。
创建完成后maven工程的结构、打包、测试都帮我们创建好,实际上就是帮我们创建一个带SpringBoot的maven工程。我们可以使用初始化器,也可以创建maven工程,使用maven的聚合、继承完成依赖传递。
4、配置文件
以前我们经常使用properties来写配置文件,但是springboot也支持我们使用yam作为配置文件,使用yaml最大的好处就是看起来比properties清爽,语法更简单,后面大量的中间件也都只会yaml作为配置文件。
yml的语法非常简单,使用一次基本知道如何使用了。使用起来一定是比xml简单,比properties清爽的。
介绍:
-
• key: value;kv之间有空格
-
• 大小写敏感
-
• 使用缩进表示层级关系
-
• 缩进不允许使用tab,只允许空格,idae中可以使用tab
-
• 缩进的空格数不重要,只要相同层级的元素左对齐即可
-
• '#'表示注释
-
• 字符串无需加引号,如果要加,’'与""表示字符串内容 会被 转义/不转义
-
对象:键值对的集合。map、hash、set、object
行内写法 : k: {k1:v1,k2:v2,k3:v3} (与json非常类似) 层次写法: k: k1: v1 k2: v2 k3: v3
-
数组:一组按次序排列的值。array、list、queue
行内写法 : k: [v1,v2,v3] 层次写法: k: - v1 - v2 - v3
示例:
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@Data
public class Pet {
private String name;
private Double weight;
}
# yaml表示以上对象
person:
userName: zhangsan
#默认使用 "" ''都是一样的效果 如果是输出 双引号会将 \n 作为换行进行输出
# 双引号不会转义,单引号会转义 \n 本身就是换行,双引号不会转义让它继续换行,单引号会转义让它原样输出
boss: false
birth: 2021/4/26 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,
但是yml默认只支持我们springboot的properties配置提示,如果想要配置更多的提示便于开发,我们就应该需要使用,导入自定义类绑定的配置提示。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
这个是我们辅助进行开发的,SpringBoot推荐我们在打包的时候安装插件,将这个排除在外,不占用JVM的内存,classloader无需加载这些类。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
此时就会有自定义的类的配置提示
5、Web开发
SpringBoot最方便的就是开发分布式的web应用,这个部分希望自己能学到框架的原理。SpringBoot是渐进式框架,web部分封装SpringMvc。
以前使用SpringMvc如果使用xml的配置还是很麻烦的,静态资源、动态资源的开启都必须配置类,还有很多功能都需要手动配置,SpringBoot帮我们自动配置了很多SpringMvc的组件。下面分析SpringBoot的web各种场景。
1、静态资源规则与定制化
1、配置静态资源位置
首先看文档,SpringBoot自动配置了静态资源的位置,并给也支持你定制化的进行配置。默认位置是类路径下的/static 、/resources 、/public 、 /META-INF/resources。
访问的时候只要 当前项目根目录/ + 静态资源名即可访问静态资源。
访问对应的静态资源都可以被映射到。
修改默认静态资源位置,配置文件默认配置,允许传入一个String类型的数组。
yml配置
spring:
web:
#修改默认的资源路径
resources:
static-locations: [ classpath:/haha/ , classpath:/abc/]
2、静态资源与动态资源的优先级
如果我们设置一个controller与静态资源同名,请求默认会先发送给controller处理动态请求而不去访问静态资源。
/**
* 与静态资源的同一映射会优先去找controller能不能进行处理
* @return
*/
@GetMapping(value = "/eva.jpg")
public String eva(){
return "eva";
}
访问结果
3、默认静态资源的请求路径
spring:
mvc:
#此时的静态资源必须加resources前缀才能进行访问
static-path-pattern: "/resources/**"
4、支持web-jars
就是将我们的传统的js文件,变成jar包,引入对应的jar包,会自动映射到静态资源。一般比较少使用这个功能。
<!-- 导入jquery的web-jars-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
后面的访问地址要去jar包里面看,包依赖的路径
总结:
1、静态资源SpringBoot帮我们配置了四个地址,我们一般使用即可,想要定制化也支持。
2、静态、动态资源同名,优先映射动态资源,所以我们一般修改静态资源的路径比如/static/**的请求都给静态资源,也方便之后nginx的动静分离。
3、SpringBoot支持webjar的形式导入js文件,使用jar配置maven的形式,简化js的导入。
4、以后开发我们一般指定静态资源的访问路径,不指定静态资源的包,使用默认的/static
2、欢迎页、favicon图标功能
修改SpringBoot应用的图标(后面版本好像取消了这个功能),只需要在静态资源的文件夹下,创建一个文件叫favicon.ico。应用的图标就就会替代成你的图标。
注:静态访问路径对welcome与图标都会有影响,后续的SpringBoot应该已经修复,这个功能平常使用也较少,了解即可。
3、SpringBoot的web场景源码分析
1、先看SpingBoot对SpringMvc的静态资源的自动配置
@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 {
观察给容器中配置了什么,有很多比如支持Rest风格的OrderedHiddenHttpMethodFilter、内容的过滤器OrderedFormContentFilter。。。先重点看其中的另一个内部配置类。
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
//配置文件绑定这两个配置类,并且绑定yml中spring.mvc 与 Spring.resources 前缀的配置
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
这个静态内部配置类,只有一个有参构造器,如果配置类中只有一个有参构造器,那么参数列表会直接从容器中进行获取。
//ResourceProperties 从容器中获取这个配置类,这个配置类绑定spring.mvc 配置
//WebMvcProperties 从容器中获取这个配置类,这个配置类绑定spring.properties配置
//ListableBeanFactory ico最底层的接口就是BeanFactory,这个配置获取ioc容器工厂
//HttpMessageConverters找到所有的消息转换器,后面再讨论这个
//ResourceHandlerRegistrationCustomizer 资源处理器的自定器
//DispatcherServletPath 处理的路径
//ServletRegistrationBean 应用注册原生的三大组件 servlet、filter、listener
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;
}
这个配置类还加入了很多的组件,比如国际化、格式等,我们着重看他是怎么配置资源管理器的。
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//静态资源的默认配置是否被禁用,默认是true不禁用
if (!this.resourceProperties.isAddMappings()) {
//如果一旦你的静态资源配置禁用,下面的配置就都不生效了 ,并打印日志
logger.debug("Default resource handling disabled");
return;
}
//获取我们配置的策略,并从缓存中获取,浏览器可以将这些静态资源缓存下来,之后要获取从缓存中获取,秒为单位。
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
//配置webjars的配置,源码说明的很明白
//如果你的注册路径是/webjars/**这样的映射,SpringBoot自动映射到类路径下的/META-INF/resources/webjars/
//也就是会去你通过maven依赖下的jar包文件中的静态资源
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));
}
//接着是访问我们其他的静态资源,这些配置都从mvcProperties中拿,并且绑定的是spring.mvc配置,你想要定制化也可以,不定制boot用默认的
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
//getStaticLocations获取到的是默认的静态资源位置,也就是boot给我们配置的那四个
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
//设置缓存,也将这些静态资源缓存给浏览器
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
禁用所有的静态资源规则,一般都不禁用。
spring:
resources:
add-mappings: true#是否禁用静态资源规则,默认不禁用
cache:
#静态资源的缓存时间
period: 1000
第一次访问是200,第二次访问就是304表名客户端这次请求用的是缓存的数据。
欢迎页的处理规则,HandlerMapping是SpringMvc的一个核心组件,就是映射处理器,保存每个Handler能处理哪些请求,然后交给适配器HanlderAdapter利用反射来处理,最后视图解析器进行视图渲染,这是MVC的流程,之后深入mvc学习的时候再去阅读源码。
@Bean
//ApplicationContext 引用上下从容器中获取
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
//创建了了一个WelcomePageHandlerMapping,我们去构造器看看,他构造了什么?
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
//WelcomePageHandlerMapping的构造器
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");
}
//如果欢迎页没有交给controller的index请求进行处理
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
小总结:
1、SpringBoot对于静态资源的管理,都是在SpringBoot的启动的时候就完成了,默认使用给你的那四个位置,你要更改就用你的。
2、默认跳转欢迎页的时候,如果不是默认规则index.html就无法处理,可以使用controller完成。
3、无论是webjars、还是其他所有静态资源,在启动后SpringBoot都给浏览器保存一份缓存,通过设置缓存的时间,单位是秒。
2、SpingBoot对请求参数的处理
web开发离不开请求映射,以前mvc开发我们习惯使用RequestMapping映射所有请求。现在的请求都流行使用RestFul风格进行开发,我们看看Rest是怎么映射的。
- Rest风格就是为了区分我们请求类型的,如果要对/User进行操作,如果都是用Get/Post,需要写四个不同的路径请求,后面维护起来就不方便,如果都是一个请求,只是请求方式不同, 那就很好区分不同请求,配合使用swagger文档,即使不写注释也能大致知道这个接口能做什么。
- 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
现在: /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户 - 以前mvc想要完成这些事情,还是挺麻烦的。先要将HiddenHttpMethodFilter 组件导入xml文件,再使用post表单提交的同时添加一个隐藏域_method="xxx"写上你需要提交的手段,这显然是有点麻烦的,每个表单都要设置,有点不合理。SpringBoot就支持自动配置Rest
WebMvcAutoConfiguration中已经完成了rest的配置,当然如果你自己配置了,SringBoot就不再帮你注册这个组件
@Bean
//是否容器中已经有这个类型的组件
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
//默认是不开启的,这个属性你需要手动开启一下
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
看这个过滤器要求我们必须使用携带隐藏域
表单设置发送RestFul请求
<form action="/user" method="post">
<!-- 想要发rest请求,设置隐藏的参数域给过滤器-->
<input type="hidden" name="_method" value="delete">
<input value="RestFul的delete删除请求" type="submit">
</form>
分析整个表单提交的rest风格(之后亲后端分离都发送ajax请求也支持rest)
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//获取原生的request请求对象
HttpServletRequest requestToUse = request;
//必须是POST请求,并且请求过程没有异常
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
//methodParam,上面提供了名字叫做_method,是final常量,从表单请求中获取请求参数_method
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
//携带了rest请求,将rest变成大写
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
//如果是规定中的rest请求创建requestToUse ,给过滤器链执行
//装饰者模式,将我们这个请求进行装饰,装饰成HttpMethodRequestWrapper,来包装rest风格请求
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
/**
* Simple {@link HttpServletRequest} wrapper that returns the supplied method for
* {@link HttpServletRequest#getMethod()}.
*/
//继承了HttpServletRequestWrapper ,我们再去HttpServletRequestWrapper看看,发现父类实现了原生的HttpServletRequest,所以这个请求也是request只是被包装了
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
//重写累了这个getMethod的方法,controller调用这个方法检查的时候,如果是rest就会返回delete、pur、patch这些请求
@Override
public String getMethod() {
return this.method;
}
}
//这个HttpServletRequestWrapper 又是原生HttpServletRequest 的实现类
//依旧是原生request请求,只是使用Wapper进行包装了
public class HttpServletRequestWrapper extends ServletRequestWrapper implements
HttpServletRequest {
//最后controller进行会调用getMethod检查请求,发现已经是被包装成delete请求了
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
总结:
1、整个表单rest的过程,使用了装饰者模式(设计模式之后有时间再去慢慢啃),简单来说就是通过过滤器将你的请求
由原来的post请求通过HiddenHttpMethodFilter交给mvc的过滤器链对请求进行过滤。
2、mvc执行请求的时候先会执行过滤链,然后才会利用反射调用目标方法,此时表单的getMethod被重写,是装饰后的HttpMethodRequestWrapper对象。
3、注意的地方,这整个过程都是对表单提交的处理,如果不是使用浏览器表单,使用其他客户端发送http请求则不是这样的。因为表单提交只支持get/post,所以才需要这样过滤。
在使用常用的http请求工具postman发送delete请求并断点试试看
postman发送请求
此时经过这个过滤器的时候,发现请求直接就是delete,无需再做包装。
最后小总结:
1、以后前后端分离开发,微服务开发都是json互相传递数据,不需要这些页面交互,所以SpringBoot设置手动开启。
2、都写@RequestMapping太麻烦,SpringMvc直接支持@GetMapping、@PostMapping、@DelteMapping等直接使用。
3、一句话这个功能就是通过拦截器装饰请求后实现的。