官网:https://spring.io/projects/spring-boot
覆盖了:web 开发、数据访问、安全控制、分布式、消息服务、移动开发、批处理…
基于 java 的版本最低为 jdk1.8
基于 Java8 的一些新特性,如:接口默认实现,重新设计源码架构
SpringBoot makes it easy to create stand-alone,production-grade
Spring based Applications that you can “just run”
能快速创建出生产级别的 Spring 应用
SpringBoot 是整合 Spring 技术栈的一站式框架
SpringBoot 是简化 Spring 技术栈的快速开发脚手架
版本迭代快,需要时刻关注变化,封装太深,内部原理复杂,不容易精通
James Lewis and Martin Fowler (2014) 提出微服务完整概念。
https://martinfowler.com/microservices/
分布式的困难:
分布式的解决:
原生应用如何上云,Cloud Native
上云的困难
服务自愈
加入 a服务在 5 台服务器上都有,3 台b 服务器,3 台 c 服务器,然后全部部署上去,突然后一天 c的一台服务器宕机了,然后c 服务能不能自愈(在别的服务器又拉起一个 c 服务)?
弹性伸缩
突然流量高峰期,a 要调用 b,b 要调用 c,c 部署的少了不够用,我们希望在 c 不够用的时候在自动的扩充 3 服务器,流量高峰期过去后将他们再下架
服务隔离
假设 c 再 1 号服务器部署,然后再 1 号服务器同时部署的可能有 d,e,f,应该希望当同一台服务器上的服务某一个出现故障后不会影响别的服务的正常运行
自动化部署
整个微服务全部部署不可能手工去部署
灰度发布
某一个服务版本有更新,如果直接将之前的版本替换成新的版本,有可能会出现故障,如果新版本不稳定,那么整个系统就会坏掉,可以先将 多个服务器中的旧版本替换为新的,验证是否能正常运行,经过长时间的验证没有出现问题则全部替换为新版本
流量治理
b 服务器性能不高,所以可以通过流量治理手段服务器只能接受少量的流量
…
https://docs.spring.io/spring-boot/docs/current/reference/html/
开发环境环境版本要求:
搭建步骤:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.2version>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
/**
* 主程序类
* @SpringBootApplication 表示这是一个 SpringBoot 应用
*/
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class);
}
}
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "Hello SpringBoot2";
}
}
运行测试
可以看到端口号默认 8080,可以创建配置文件,再配置文件中进行修改:server.port=8088
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.5.2version>
parent>
几乎声明了所有开发中常用依赖的版本号,自动版本仲裁机制
在 pom.xml 中见到很多的 spring-boot-starter-*,只要引入 starter,这个场景的所有常规需要的依赖都会自动引入
类似于 :*-spring-boot-starter,这种是第三方为我们提供的简化的依赖
所有的场景启动器最底层的依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
<version>2.5.2version>
<scope>compilescope>
dependency>
引入依赖默认都可以不写版本号
引入非版本仲裁的jar 要加入版本号
可以修改默认的版本号
可以修改默认的版本号
首先查看默认配置的版本号使用的方式,然后使用key 进行修改
<properties>
<mysql.version>5.1.4mysql.version>
properties>
自动配置好了 Tomcat
我们只需要在配置文件中设置tomcat 的属性就可以了
自动配置好 Web 常见功能,如:字符编码问题
在之前 SpringMVC 开发中,需要在 web.xml 中配置 characterEncodingFilter
,在 SpirngBoot 中只需要引入 web 的启动器就可以了。
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class);
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
自动配置好包结构
在 SpringMvc中需要在 xml 文件中设置 componentScan,SpringBoot中无需配置
主程序所在包及其下面的所有子包里面的组件都会被默认扫描(约定的规则)
无需以前的包扫描配置
要想改变扫描的路径
@SpringBootApplication(scanBasePackages = "xx.xx.xx")
或者使用注解 @ComponentScan
指定扫描路径
各种配置拥有默认值
按需加载所有自动配置项 Conditional
spring-boot-autoconfigure
包里面。<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
<version>2.5.2version>
<scope>compilescope>
dependency>
@Configuration
Full 模式与 Lite 模式
配置类组件之间无依赖关系用 Lite 模式加速容器启动过程,配置类组件之间有依赖关系,方法会调用得到之前单实例组件,用 Full 模式
假设有一个 User 类,想要注册组件
<bean id="user1" class="com.example.pojo.User">
<property name="name" value="zhangsan">property>
bean>
/**
* 1. 配置类里面使用 @Bean 标注在方法上给容器注册组件,默认也是单实例的
* 2. 配置类加上 @Configuration 也是一个组件
* 3. proxyBeanMethods:代理 bean 的方法,如果为 true 外部无论对配置类中的这个组件注册方法调用多少次
* 获取的都是之前注册容器中的单实例对象。如果为 true 都会去容器中找组件
* Full(proxyBeanMethods = true)
* Lite(proxyBeanMethods = false) 为 false 组件在容器中不会保存代理对象,每一次调用都会产生一个新的对象
* 解决组件依赖的场景
* 组件依赖必须使用 Full 模式(默认),其他默认是否 Lite 模式
* 4. 如果是 false ,SpringBoot 不会检查容器中方法返回的东西是否存在,提高运行的效率
* 如果是 true,则每次执行都会检查
* 5. 如果只是向容器中配置组件,别人也不依赖这个组件则设置成 false
* 如果组件在下面别人还要用就设置为true,保证容器中的组件就是要依赖的组件
*/
@Configuration(proxyBeanMethods = true) // 告诉 SpringBoot 这是一个配置类 == SpringMvc 中的 beans.xml
public class MyConfig {
@Bean // 给容器中添加组件,id 为方法名,返回类型为组件类型,返回值就是组件在容器中的实例
public User user1() {
return new User("zhangsan");
}
}
@Bean、@Component、@Controller、@Service、@Repository
与 SpringMvc 中使用方法一样
@ComponentScan、@Import
@ComponentScan 就是配置包扫描的
@Import 给容器中导入一个组件
在容器中组件上面使用
在我的配置文件中导入 User 和 DispatcherServlet两个组件
@Import({User.class, DispatcherServlet.class})
@Configuration
public class MyConfig {
}
然后在启动类中得到 bean 然后输出查看
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class);
// 因为可能有多个,所以返回值结果为一个数组类型
String[] beanNamesForType = run.getBeanNamesForType(User.class);
for (String s : beanNamesForType) {
System.out.println(s);
}
String[] beanNamesForType1 = run.getBeanNamesForType(DispatcherServlet.class);
for (String s : beanNamesForType1) {
System.out.println(s);
}
}
}
// 自定义逻辑返回需要导入的组件
// 由于实现了 ImportSelector,所以把注册的方法的全类名返回
public class MyImportSelector implements ImportSelector {
// 返回值,就是要导入到容器中的组件全类名
// AnnotationMetadata:当前标注 @Import 注解的类的所有注解信息
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 方法不要返回 null 值,可以返回一个 空空数组
return new String[]{"com.example.pojo.Test1","com.example.pojo.Test2"};
}
}
//2. ImportSlector:返回需要导入的组件的全类名数组
@Import({User.class, DispatcherServlet.class,MyImportSelector.class})
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata:当前类的注释信息
* @param registry:BeanDefinition 的注册类
* 把所有需要添加到容器中的 Bean,
* 调用BeanDefinitionRegistry.registerBeanDefinition手工注册进来
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 指定 bean 名
boolean test1 = registry.containsBeanDefinition("com.example.pojo.Test1");
boolean test2 = registry.containsBeanDefinition("com.example.pojo.Test2");
if (test1 && test2) {
// 指定 Bean 定义信息(Bean 的类型,bean 的作用域都可以在这里指定)
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Test1.class);
// 注册一个 bean,指定 bean 名为 test
registry.registerBeanDefinition("test",rootBeanDefinition );
}
}
}
条件装配:满足 Conditional 指定条件,则进行组件的注入
@Configuration
public class MyConfigConditional {
@ConditionalOnBean(name = "pet")
// @ConditionalOnBean(name = "pet") 如果 pet 组件注册到容器中,则 user 组件也会被注册到容器中
// 如果 pet 没有 @Bean 注册,则 user 组件也不会注册到容器中
// @ConditionalOnBean 是容器如果有某一个组件,就会将加本注解的组件注册到容器,条件不成立不会将组件注册到容器
@Bean
public User user() {
return new User("张三");
}
//@Bean
public Pet pet() {
return new Pet();
}
}
在 SpringMvc 模式下有一个配置文件,然后再配置文件中有很多的 bean 标签注册了很多的组件
然后再 SpringBoot 中想使用这些组件不用一个一个进行修改,只需要再要使用的类上使用注解
@ImportResource("classpath:beans.xml")
我们习惯于把经常爱变化的东西配置到配置文件中,比如数据库的已连接地址账号密码等,之前的操作中我们需要加载配置文件,然后得到每一个 key value 的值,然后把这些 k v 值一一对应封装到 JavaBean 中。在 SpringBoot 中这个过程会变得非常简单,这个过程就叫做配置绑定。
方式一:@ConfigurationProperties
car.brand=BMcar.price=1999
在 Car 类上面进行绑定、
注意:只有在容器中的组件,才能使用这个配置绑定的功能
@Component
/**
* 只有在容器中的组件,才能使用这个配置绑定的功能
*/
@Component
@ConfigurationProperties(prefix = "car")
public class Car {
private String brand;
private Integer price;
// 省略 getter / setter 方法
}
@Autowired
private Car car;
@RequestMapping("/car")
public Car car() {
return car;
}
方式二:@EnabledConfigurationProperties + @ConfigurationProperties
因为我们有时候可能需要使用第三方的组件,而这些组件我们是不能使用 @Component 注册到容器中的,所以可以使用这种方式
@Configuration
@EnableConfigurationProperties(Car.class)
// 1. 开启 Car 配置绑定功能
// 2. 把这个 Car 这个组件自动注册到容器中
public class MyConfig {
}
@SpringBootApplication 点进去是一个合成注解的类
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@SpringBootConfiguration
点进去看到一个有 @Configuration 注解的类,代表当前就是一个配置类,也就是 main 程序也就是 Spring 中的一个核心配置类
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
@ComponentScan
表示指定要扫描那些包
@EnableAutoConfiguration
点击后可以看到也是一个合成注解
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
@AutoConfigurationPackage
自动配置包,指定了默认的包结构的规则
这就解释了为啥 MainApplication 所在的包的注解才能生效
// 给容器中导入一个组件,这里的 Register 是给容器中批量注册组件
// 将指定的一个包下的所有组件导入进来
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
利用 getAutoConfigurationEntry(annotationMetadata);
方法给容器中批量导入一些组件,获取所有配置的集合
这个方法会先将得到的所有组件去掉重复的,移除一些没有用到的等等操作,然后返回。
getCandidateConfigurations(),利用 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;
}
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
classLoader.getResources(FACTORIES_RESOURCE_LOCATION); 这个会加载资源文件,文件位置点进去就可以看到
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// 这个方法最终会加载得到所有的组件
从 META-INF/spring.factories
位置来加载一个文件,默认扫描我们当前系统里面所有 META-INF/spring.factories 位置的文件
在这个文件里面就是所有自动配置的东西,通过这个机制进行自动配置,其实就是在配置文件中写死的
意思就是这个文件中写死了 spring-boot 一启动就要给容器中加载的所有配置类
虽然自动配置项在启动的时候会默认全部加载,但是最终会按照条件装配规则按需装配的。
以 DispatcherServletAutoConfiguration 为例
首先查看类上面的 Conditioinal 注解生效后,然后查看方法上面的 Conditional 注解生效,然后就会注册这个 bean,为什么使用 dispatcherServlet 不会进行一些别的操作,因为在这里Spring 已经为我们创建好了对象,并且做了一系列的配置然后返回 dispatcherServlet
@EnableConfigurationProperties(WebMvcProperties.class)
这里就是对 application.properties 文件进行一个绑定
这个就是我们要导入的配置文件,他会从我们的 application.properties 文件中找到相应的自己的配置,然后进行配置的设置
@ConfigurationProperties(prefix = “spring.mvc”)
通过 spirng.mvc 这个前缀就可以修改默认的配置信息
@Bean
@ConditionalOnBean(MultipartResolver.class) // 判断容器中有这个类的组件,条件判断是否生效
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)// 如果容器中没有 multipartResolver 这个组件生效,如果有的话就是用户自己定义了自己的组件,然后不生效
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// @Bean 标注的方法传入到了对象参数,这个参数的值就会从容器中找
// multipartResolver ,防止有一些用户配置的文件上传解析器不符合规范
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
大概就是这个意思,可能不太准确:这个方法并没有进行什么设置,只是怕有的人使用的时候把文件上传的名字写错了,然后这里 MultipartResolver resolver 接受到传入的值,然后进行一个相当于重命名的操作进行 return,multipartResolver 这个名字就是注册好的 bean
public static final String MULTIPART_RESOLVER_BEAN_NAME = “multipartResolver”;
SpringBoot 默认会在底层配置好所有的组件,但是说如果用户使用 @Bean 等一些注解配置了自己的组件,则以用户配置的组件优先 @ConditionalOnMissingBean
SpringBoot 先加载所有的自动配置类 xxxxAutoConfiguration
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值
从 xxxxProperties 里面拿到,xxxxProperties 和配置文件进行了绑定
生效的配置类就会给容器中装配很多组件
只要容器中有这些组件,相当于这些功能就已经是实现了
自定义配置
xxxxAutoConfiguration —> 组件 —> xxxxProperties 里面拿值 ----> application.properties
引入场景的依赖
查看自动配置了那些(选择查看)
https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.auto-configuration
debug = true
开启自动配置报告。
修改配置项目
自定义加入或者替换的组件
在我们床架实体类后我们需要手动生成它的有/无 参构造方法、get/set 方法,toString 方法等,使用 Lombok 后只需要使用简单的注解可以实现以上的内容
在 Spring Boot 父依赖中找到,然后在 pom.xml 文件中引入
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
@Data // 生成 get/set 方法
@ToString // 生成 toString 方法
@AllArgsConstructor // 生成全参构造器
@NoArgsConstructor // 生成无参构造器
public class User {
private String name;
}
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
log.info("请求进到这里来了。。。。。");
return "Hello SpringBoot2";
}
}
启动浏览器访问这个接口后,控制台会输出我们给定的日志信息
快速的创建好 Spring Boot 应用
选择好场景之后,我们点击 Next ,然后 idea 就会联网把我们的项目下载好
热更新,我们在做项目的时候可能会进行修改,然后不想每次启动后去点击启动按钮,可以使用 dev-tools 热更新,修改完代码后使用 Ctrl + F9(对项目重新编译一下,然后重新加载) 就可以实时更新了
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
dependencies>
在之前使用过的 properties 对属性继续配置,配置名=值
YMAL 是 “YAML Aint’s Markup Language” (YAML 不是一种标志语言)的递归缩 写。在开发的这种语言时,YMAL 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。
key: value
行内写法: k: {k1:v1,k2:v2,K3:v3}
或者
k:
k1: v1
k2: v2
k3: v3
行内写法: k: [v1,v2,v3]
或者
k:
- v1
- v2
@ConfigurationProperties(prefix = "person")
@Component
@Data
@ToString
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;
}
@Component
@ToString
@Data
public class Pet {
private String name;
private Integer age;
}
person:
userName: 张三
boss: true
birth: 2020/09/23
age: 20
pet:
name: 小黄
age: 10
interests:
- 篮球
- 足球
animal:
- 小猫
- 小狗
score:
语文:
first: 33
second: 44
third: 55
数学: [30,50,89]
salarys:
- 20000
- 1000
allPets:
sick:
- name: 小猫
age: 10
health:
- name: 小狗
age: 30
在编写 yaml 文件的时候,输入的时候是没有提示信息的,在项目中加入配置处理器
官方地址:https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html#configuration-metadata.format
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
重启项目
然后在 yaml 中输入就会有提示信息
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
exclude>
excludes>
configuration>
plugin>
plugins>
build>
一个源码分析的链接:https://www.cnblogs.com/seazean/p/15109440.html
官网内容:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications
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:
ContentNegotiatingViewResolver
and BeanNameViewResolver
beans.
Converter
, GenericConverter
, and Formatter
beans.
Converter
, GenericConverter
, and Formatter
HttpMessageConverters
(covered later in this document).
HttpMessageConverters
MessageCodesResolver
(covered later in this document).
MessageCodesResolver
,国际化用index.html
support.
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
自定义规则
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
改变默认底层组件
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
官网地址:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications.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
.
/static
(or /public
or /resources
or /META-INF/resources
) 这些文件夹下,然后访问我们当前项目的根路径就可以访问到静态资源了,因为它使用了 ResourceHttpRequestHandler
处理了只要我们将静态资源放在类路径下的:/static /public /resources /META-INF/resources 文件下,我们就可以通过当前项目的根路径/ + 静态资源名来进行访问。
测试访问根目录下的资源
localhost:8080/资源名字.扩展名
就可以访问到假设我们 controller 中的动态请求路径与根目录下静态资源的名字相同
在这种情况下,当请求进来的时候,会先去 Controller 中看能不能进行处理,不能处理的请求又都交给资源静态处理器,因为静态资源映射的是 /**,所以会在根目录下找相应的静态资源,如果静态资源也找不到就会响应 404 页面。
静态资源访问前缀
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:
默认情况下,映射的路径是 /**,也就是说访问我们的静态资源只需要写静态资源名就会自动的找到静态资源。如果想要改变这个静态资源访问的路径,可以通过修改 spring.mvc.static-path-pattern
实现,也就是说给请求加一个前缀,以防止项目中使用拦截器静态资源会被拦截
spring:
mvc:
static-path-pattern: /res/**
# 表示 /res 下面的都是静态资源请求,在访问静态资源的时候就访问这个地址加静态资源名
改变默认的静态资源路径
spring:
web:
resources:
static-locations: classpath:/myresources
指定了 static-locations 后,所有请求的静态资源文件都会去指定的文件加下找,别的位置找不到的。
支持 webjars
官网:https://www.webjars.org/
webjars 就是将 jquery、js、css 等静态文件打包成 jar 包
以 jquery 为例测试
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>jqueryartifactId>
<version>3.5.1version>
dependency>
Spring Boot supports both static and templated welcome pages. It first looks for an index.html
file in the configured static content locations. If one is not found, it then looks for an index
template. If either is found, it is automatically used as the welcome page of the application.
Spring Boot 既支持静态欢迎页面,也支持模板欢迎页面。如果我们是前者,我们将 index.html 静态资源文件放到静态资源路径下,就会被当成欢迎页面,也就是访问项目根路径默认展示的页面。或者静态资源路径下没有存在这个页面,也会给我们找 index 这个模板(有一个 Controller 处理 index 请求,最终跳回页面,这个 index 模板最终也会作为我们的欢迎页)
静态资源路径下 index.html 页面
这里我们可以在 yml 文件中配置自己的静态资源路径,然后将我们的 index.html 页面放到自己定义的静态资源文件下,但是不可以配置静态资源访问路径,否则导致 index.html 不能被默认访问;也可以放到默认生成的 static 静态资源目录下,然后通过 localhost:8080 就可以访问到我们的欢迎页
Controller 根据请求处理 /index 跳转到 欢迎页
每一个网站都有一个自己的图标,例如 Spring 官网的:
这个配置好像在 2.3.x 版本后就没有了
如果要使用,只需要把图标改名为 favicon.ico
放到静态资源目录下就可以了,就会被自动配置为应用的图标
WebMvcAutoConfiguration
这个类中查看 WebMvcAutoConfiguration 是否生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
查看 WebMvcAutoConfiguration 给容器中配置了什么东西
OrderedHiddenHttpMethodFilter
SpringMvc 为了兼容 RestFul 风格的
OrderedFormContentFilter
表单内容的过滤器
WebMvcAutoConfigurationAdapter
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
// 配置文件的相关属性和 xxx 进行了绑定
@EnableConfigurationProperties({ WebMvcProperties.class,
org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
WebMvcProperties
与前缀 spring.mvc
的配置文件进行绑定
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
ResourceProperties
与前缀 spring.resources
的配置文件进行绑定
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties extends Resources {
注意: 一个配置类只有一个有参构造器,特性:有参构造器中所有参数的值都会从容器中确定
// ResourceProperties resourceProperties 获取所有和 `spring.resources` 绑定的所有值的对象
// WebMvcProperties mvcProperties 获取所有和 `spring.mvc` 绑定的所有值的对象
// ListableBeanFactory beanFactory 相当于是找的 IOC,容器工厂 bean 工厂,找 Spring 的容器
// HttpMessageConverters 找到所有的 HttpMessageConverters
// ResourceHandlerRegistrationCustomizer 找到资源处理器的自定义器
// DispatcherServletPath 处理的路径
// ServletRegistrationBean 给应用注册原生的 servlet,filter 等
public WebMvcAutoConfigurationAdapter(
org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,
ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties
: webProperties.getResources();
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
资源处理的默认规则
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
// webjars 规则
addResourceHandler(registry, "/webjars/**", "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);
}
});
}
if (!this.resourceProperties.isAddMappings()) {
isAddMapping 点进去,可以得到信息:private boolean addMappings = true;
,这里默认是true ,我们可以在 yaml 文件中设置为false ,如果为 false 则表示过滤掉所有静态资源的请求
因为这个方法的下面都是静态资源的配置的信息,所以如果设置为 false 则直接return,不会向下执行,所以可以理解为过滤掉所有静态资源
spring:
resources:
add-mappings: false #禁用掉所有静态资源
注册第一种访问规则 addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
,这就是为什么访问 webjars 文件夹下的静态资源可以直接访问
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration)
静态资源路径的配置规则
this.mvcProperties.getStaticPathPattern()
在 WebMvcProperties 中,这个文件与 prefix = “spring.mvc” 绑定的
然后在 this.resourceProperties.getStaticLocations() 找静态资源的路径 private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/","classpath:/public/" };
欢迎页的处理规则
HandlerMapping 处理器映射,里面保存了每一个 Handler 能处理那些请求,请求一过来 HandlerMapping 就会看,那个请求交给谁处理,找到以后用反射调用可以处理的方法
@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(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
// 这里可以得到信息,要使用 欢迎页 就必须配置路径为 /**
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
// 否则调用 Controller 看能不能处理这个请求
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
@xxxxMapping
Rest 风格支持(使用 Http 请求方式动词来表示对资源的操作)
之前在 MVC 中使用 Rest 风格的时候,我们需要在配置一个 HiddenHttpMethodFilter
假设没有配置 HiddenHttpMethodFilter
因为在 html 表单提交之后 get 和 post 两种方式
<form action="/user" method="get">
<input type="submit" value="GET-提交">
form>
<form action="/user" method="post">
<input type="submit" value="POST-提交">
form>
<form action="/user" method="delete">
<input type="submit" value="DELETE-提交">
form>
<form action="/user" method="put">
<input type="submit" value="PUT-提交">
form>
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser() {
return "GET-用户";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String postUser() {
return "POST-用户";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser() {
return "DELETE-用户";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser() {
return "PUT-用户";
}
HiddenHttpMethodFilter
,这种情况下 get 访问会请求 get 的controller,post 访问会请求 post 的controlller,但是如果如果使用 delete、put 则都会走 get 请求,因为 html 中表达的提交方式只有两种,如果请求方式不是这两种就默认以 get 方式请求处理解决方式:
在 SpringBoot 中已经配置好了 HiddenHttpMethodFilter
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
点进去我们会发现
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS =
Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
/** Default method parameter: {@code _method}. */
// 这里表示我们只需要带一个隐藏的 _method 项就可以使用 Rest 风格
public static final String DEFAULT_METHOD_PARAM = "_method";
注意:在html 总 form 请求的时候 method 必须是 post 方式,因为在 doFilterInternal 中只有 POST 请求才能生效 "POST".equals(request.getMethod()
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 如果是 post 请求,就会拿到 methodParam = DEFAULT_METHOD_PARAM = "_method"
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
根据上面得到的信息,在 html 中加上隐藏 并且设置 _method,发现还是不行
<form action="/user" method="get">
<input type="submit" value="GET-提交">
form>
<form action="/user" method="post">
<input type="submit" value="POST-提交">
form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="DELETE"/>
<input type="submit" value="DELETE-提交">
form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT"/>
<input type="submit" value="PUT-提交">
form>
然后我们看到 @ConditionalOnProperty(prefix = “spring.mvc.hiddenmethod.filter”, name = “enabled”),可能默认值是 false
在 yaml 文件中将这个属性的值设置为 true 就可以正常使用 Rest 风格进行访问了
spring:
mvc:
hiddenmethod:
filter:
enabled: true
Rest 原理 — 基于表单提交使用 Rest
首先,提交表单的时候会带上 _method
参数,以及真正提交方式的参数
在 Spring Boot 有过滤器,所以当请求过来的时候会被 HiddenMethodFilter
拦截
然后处理请求
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request; // 原生的请求
// "POST".equals(request.getMethod()) 然后判断原生的请求方式是不是 POST,这就是为什么使用 delete 和 put 的时候要求请求方式是 POST 才能使用 Rest 风格
// request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) 判断我们当前的请求中有没有错误
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// request.getParameter 在原生的请求中能获取到请求的参数,即获取到 _method 参数的值
// methodParam = "_method"
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
// 然后将 _method 参数的转换成大写,也就是说表单提交的参数 delete 大小写无所谓
String method = paramValue.toUpperCase(Locale.ENGLISH);
// 判断它们允许(除了 get和 post 外 兼容 PUT、DELETE、PATCH)的请求方式中包不包含提交的请求
if (ALLOWED_METHODS.contains(method)) {
// 原生 request(post) 包装模式 requestWrapper 重写了 getMethod 方法,返回的是传入的值
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
// 过滤器链放行的时候用 wrapper,以后的方法调用 getMethod 是调用 requestWrapper 的
filterChain.doFilter(requestToUse, response);
}
上面 Controller 请求方式的切换
扩展:把 _method 自定义
自定义一个 WebConfig 类,注册自己的 HiddenMethodFilter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}
在 Spring Boot 中,所有的请求都会到 DispatherServlet
中,其实 SpringBoot 底层使用的还是 SpringMVC
DispatcherServelt 继承 FrameworkServelt 继承 HttpServelt
当请求开始的时候 HttpServlet 的doGet 最终都会调用到 FrameServlet 中的 processRequest,在 processRequest 中又去调用 doService,在最终的 DispatcherServlet 中对 doService 进行了实现
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// HttpServlet 的doGet 最终都会调用到 FrameServlet 中的 processRequest
processRequest(request, response);
// 点进去这个方法发现又调用了本类的 doService 方法
}
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
// 这是核心部分
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
进去 doService 发现是一个抽象方法,然后去 DispatcherServlet 中找到对应的方法
protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
throws Exception;
DispatcherServlet 最终对 doService 做了实现,然后发现 doService 中又调用了 doDispatch 方法,而这个 doDispatch 方法就是请求映射的核心内容,每个请求都会调用 doDispatch 方法
doDispatch 所有 SpringMVC 功能分析都从这个方法开始
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 来处理我们的请求
mappedHandler = getHandler(processedRequest);
进入 getHandler 中我们会看到 HadnlerMapping(处理器映射) 有 5 个,也就是说我们的 这里面保存到了我们的映射规则, /*** 由谁处理等
这里有 5 个 HandlerMapping ,启动后会遍历这 5 个查找看谁能处理这个请求
所有的请求映射都保存在 HandlerMapping 中
SpringBoot 中自动除了配置欢迎页的 HandlerMapping。访问 / 能访问到 index.html
请求进来的时候,会逐个查看所有的 HandlerMapping 是否有请求的信息
如果有就找到这个请求对应的 handler
如果没有就找下一个 HandlerMapping
上面这些 HandlerMapping 都可以在 WebMvcConfiguration 类中找到
HandlerMapping 其实就是保存那个请求由谁进行处理
第一部分:注解
@GetMapping("/user/{name}")
public Map testParam(@PathVariable String name) {
HashMap<Object, Object> map = new HashMap<>();
map.put("name",name);
return map;
}
@GetMapping("/user/{name}")
public Map testParam(@RequestHeader Map<String,String> head) {
HashMap<Object, Object> map = new HashMap<>();
map.put("head",head);
return map;
}
@GetMapping("/user1")
public Map testParam(@RequestParam("name") String name) {
HashMap<Object, Object> map = new HashMap<>();
map.put("name",name);
return map;
}
@GetMapping("/user1")
public Map testParam(@CookieValue("_ga") String _ga) {
HashMap<Object, Object> map = new HashMap<>();
map.put("_ga",_ga);
return map;
}
<form action="/save" method="post">
<input name="name" />
<input name="age" />
<input type="submit" value="提交" />
form>
@PostMapping("/save")
public Map save(@RequestBody String content) {
HashMap<String, Object> map = new HashMap<>();
map.put("content",content);
return map;
}
模拟页面的跳转
@Controller
public class RequestAttributeController {
@GetMapping("/goto")
public String goTo(HttpServletRequest request) {
request.setAttribute("name","张三");
request.setAttribute("age",20);
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public Map successTest(@RequestAttribute("name") String name,
@RequestAttribute("age") Integer age,
HttpServletRequest request) {
String nameRequest = (String) request.getAttribute("name");
HashMap<String, Object> map = new HashMap<>();
map.put("nameRequest",nameRequest);
map.put("nameAnnotation", name);
return map;
}
}
测试运行
矩阵变量需要在 SpringBoot 中手动开启,还应当绑定在路径变量中,若是有多个矩阵变量,应当使用英文符号;进行分割,若是一个矩阵变量有多个值,应当使用英文符号进行分割,或者命名多个重复的 key即可。
启动矩阵变量
在 WebMvcConfiguration 中找到方法 public void configurePathMatch(PathMatchConfigurer configurer) {
,进入url 路径帮助器 UrlPathHelper urlPathHelper = new UrlPathHelper();
,然后可以看到private boolean removeSemicolonContent = true;
这里有一个属性是移除分号的,默认的true
/**
* 分号要是移除就会把 url 中分号后面的内容全部都去掉,即忽略了参数
* Set if ";" (semicolon) content should be stripped from the request URI.
* Default is "true".
*/
public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
checkReadOnly();
this.removeSemicolonContent = removeSemicolonContent;
}
对于路径的处理都是 UrlPathHelper 进行解析的,
removeSemicolonContent --移除分号内容
解决方法:
自定义我们自己的 UrlPathHelper
@Controller
// 实现接口,重写方法,设置为 false
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
在之前的访问路径中我们使用 /user/{id}?xxx=xxx&xxx=xxx
,只用 RequestParam 获得参数
矩阵变量:
// 1. /cars/sell;low=34;brand=byd,audi,yd 访问路径方式
/cars/sell;low=34;brand=byd;brand=audi;brand=yd
// 2. SpringBoot 中默认是禁用了矩阵变量功能,需要手动开启矩阵变量的url 的路径变量才能被解析
// 3. 矩阵变量必须有 url 路径变量才能被解析,如果直接写路径会找到 404
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand) {
HashMap<String, Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
return map;
}
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge) {
HashMap<Object, Object> map = new HashMap<>();
map.put("boossAge",bossAge);
map.put("empAge",empAge);
return map;
}
进入 DispatcherServlet 类中,找到 doDispatch 方法,然后
首先在 HandlerMapping 中找到处理请求的 Handler,为当前 Handler 找一个适配器 HandlerAdapter
// Determine handler adapter for the current request.
// 决定一个 Handler 的适配器为当前请求
// 在此之前我们已经找到那个方法能够处理这个请求了
// SpringMVC 需要在底层通过反射调用controller 中的方法,以及一大堆的参数,SpringBoot 就把这些封装到了 HandlerAdapter 中,相当于这就是一个大的反射工具。
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
然后找到了 4 个 HandlerAdapter(处理器适配器,可以完成不同的功能)
支持方法上标注 @RequestMapping 这些注解的适配器
支持函数式编程的适配器
进入 supports 方法,这里会把 handler 封装一个 HandlerMethod
@Override
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
返回的是一个 RequestMappingHandlerAdapter
至此,找到的请求的适配器
DiapatcherServlet 的 doDispatche 方法中
// Actually invoke the handler.
// 真正的执行 handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
进去 ha.handle,然后进入 handlerInternal
对于目标方法的真正执行都在 RequestMappingHandlerAdapter 类的 handleInternal 方法中
向下走到 invokeHandlerMethod 方法
// No synchronization on session demanded at all...
// 执行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);
argumentResolvers
参数解析进到 invokeHandlerMethod 方法可以看到 27 个参数解析器 argumentResolvers
执行目标方法的核心关键会设置参数解析器,将来目标方法的每一个参数值是什么是由这个参数解析器确定的,确定将要执行的目标方法的每一个参数值是什么
SpringMVC 目标方法能写多少种参数类型,取决于参数解析器
这个参数解析器其实就是一个接口:HandlerMethodArgumentResolver,
这个接口中,接口第一个方法 supportsParameter 判断接口是否支持这个方法,即当前解析器支持解析那种参数。
如果支持就调用 resolveArgument 解析方法进行解析
从这里我们可以看到目标方法可以写多少种类型的返回值
SpringMVC 会提前把参数解析器和返回值处理器都放到一个目标方法包装的 ServletInvocableHandlerMethod 这个可执行的方法中
向下找到方法 invocableMethod.invokeAndHandle(webRequest, mavContainer);
执行并处理方法来执行目标方法,invocableMethod 里面封装了各种处理器
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 这个方法执行的时候进入目标方法,然后再向下执行,真正执行目标方法
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
进入 invokeForRequest(webRequest, mavContainer, providedArgs);
方法中
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 获取方法的所有参数的值 确定方法参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
// 真正的如何确定目标方法的每一个值
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 获取所有参数的参数声明
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
// 所有目标方法确定好的值
Object[] args = new Object[parameters.length];
// 遍历所有参数
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
// 给 args 赋值
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 首先判断当前解析器是否支持这中参数类型
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
// 所有目标方法确定好的值
return args;
}
确定目标方法的参数值
首先遍历判断所有参数解析器那个支持解析这个参数
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
解析参数的值
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
// 得到参数变量
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
// 确定值
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
所有支持的注解的类型 HandlerMethodArgumentResolver
WebRequest、ServletRequest、MultipartRequest、HttpSession、java.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、Zoneld
ServletRequestMethodArgumentResolver
以上的部分参数
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Erros/BindingResult、RedirectAttributes(重定向携带数据)、ServletResponse(response 响应)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request
// 都是可以给 request 域中放数据,以后方便 request.Attribute 获取
原理
Map、Model 类型的参数,会返回 mavContainer.getModel(); --> BindinigAwareModelMap 是 Model 也是 Map
无论是 Map 还是 Model 类型最终都会调用这个方法 mavContainer.getModel(); 获取到值的
public class ModelAndViewContainer {
private boolean ignoreDefaultModelOnRedirect = false;
@Nullable
private Object view;
private final ModelMap defaultModel = new BindingAwareModelMap();
解析完参数后会进行转发,
InvocableHandlerMethod.java
类中执行 this.returnValueHandlers.hanleReturnValue 进行返回值的处理
解析参数的值后,将所有的数据都放在 ModelAndViewContainer中,包含要去的页面地址 View,还包括 Model 数据。
视图解析就是 SpringBoot 在处理完请求之后来跳转到某个页面的这个过程。
视图解析:因为 SpringBoot 默认打包方式是一个jar包即压缩包,jsp 不支持打包成压缩包,所以 SpringBoot 默认不支持 jsp,需要引入第三方模板引擎技术实现页面的渲染
经常使用的方式就是处理完请求之后进行转发或者重定向到一个指定的视图页面
视图解析的原理过程
目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer
里面。包括数据和视图地址
方法的参数是一个自定义类型对象(从请求参数中确定的),把它重新放在 ModelAndViewContainer
任何目标方法执行完成以后都会返回 ModelAndView
(数据和视图地址)
processDispatchResult
处理派发结果(页面该如何响应)
render
(mv、request、response); 进行页面渲染逻辑
根据方法的 String 返回值得到 View 对象[定义了页面的渲染逻辑]
所有的视图解析器尝试是否能根据当前返回值得到 View 对象
得到了 redirect:/main.html --> Thymeleaf new RedirectView()
ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象
view.render(mv.getModelInternal(),request,response); 视图对象调用自定义的 render 进行页面渲染工作
视图解析:
官网:thymeleaf.org
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of
processing HTML, XML, JavaScript, CSS and even plain text.
thymeleaf 是一个现代化的服务端的 Java 模板引擎。
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${} | 获取请求域、session域、对象等值 |
选择变量 | *{} | 获取上下文对象值 |
消息 | #{} | 获取国际化等值 |
链接 | @{} | 生成链接 |
片段表达式 | ~{} | jsp:include 作用,引入公共页面片段 |
字面量
文本值:‘text’
数字:0,44,3.3
布尔值:true,false
空值:null
变量:value,key
文本操作
字符串拼接:+
变量替换:|My name is $(name)|
数字运算
+,-,*,/,%
布尔运算
and,or,!,not
比较运算
< > >= <= (gt,lt,ge,le)
等式:== != (eq,ne)
条件运算
if-then:(if)?(then)
if-then-else:(if)?(then):(else)
Default:(value)?:(defaultvalue)
设置属性值 th:attr
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
fieldset>
form>
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onionstd>
<td th:text="${prod.price}">2.41td>
<td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onionstd>
<td th:text="${prod.price}">2.41td>
<td th:text="${prod.inStock}? #{true} : #{false}">yestd>
tr>
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">viewa>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administratorp>
<p th:case="#{roles.manager}">User is a managerp>
<p th:case="*">User is some other thingp>
div>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
自动配置好的东西,在 ThymeleafAutoConfiguration 类中可以看到
private final ThymeleafProperties properties;
SpringTemplateEngine engine = new SpringTemplateEngine();
ThymeleafViewResolver
使用的时候只需要开发页面就可以了。
在 ThymeleafProperties
类中可以看到已经配置好的前缀和后缀
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
// 前缀,templates 这个文件夹在创建项目的时候文件夹已经创建好了
public static final String DEFAULT_PREFIX = "classpath:/templates/";
// 后缀,默认都是 xxxx.html 页面
public static final String DEFAULT_SUFFIX = ".html";
@Controller
public class ViewController {
@GetMapping("/thymeleaf")
public String toPage(Model model) {
// model 中的数据会被放到请求域中,request.setAttribute("xxx","xxx");
model.addAttribute("message","Hello,World");
model.addAttribute("url","http://www.baidu.com");
return "success";
}
}
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1 th:text="${message}">Thymeleaf 你好h1>
<a href="www.baidu.com" th:href="${url}">百度a>
body>
html>
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
使用步骤
HandlerInterceptor
接口,拦截器中写上拦截规则/**
* 登录检查
* 1. 配置好拦截器要拦截那些请求
* 2. 把这些配置放在容器中
*/
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登录检查
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null) {
// 放行
return true;
}
// 拦截,拦截住的都是为登录的,跳转都登陆页
request.setAttribute("msg","请登录后请求");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成之后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 页面渲染以后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 所有请求都被拦截,包括静态资源
.excludePathPatterns("/","/login","/css/**","/data/**","/font-awesome/**","images/**","/js/**","/lib/**","/plugins/**");
}
}
文件上传表单
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<label>单个文件label>
<input type="file" name="headImage">
<label>多个文件label>
<input type="file" name="photos" multiple>
<input type="submit" value="提交">
form>
/**
* 测试文件上传
*/
@Slf4j
@Controller
public class FormController {
@GetMapping("/form")
public String toFormPage() {
return "form/forms-upload";
}
/**
* MultipartFile 自动封装上传过来的文件
*
* @param name
* @param age
* @param headImage
* @param photos
* @return
*/
@PostMapping("/upload")
public String upload(@RequestParam("name") String name,
@RequestParam("age") Integer age,
@RequestPart("headImage") MultipartFile headImage,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("name={},age={},headImage={},photoSize={}", name, age, headImage.getSize(), photos.length);
if (!headImage.isEmpty()) {
// 保存到文件服务器:OSS 服务器
String filename = headImage.getOriginalFilename();
headImage.transferTo(new File("D:\\" + filename));
}
if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String filename1 = photo.getOriginalFilename();
photo.transferTo(new File("D:\\" + filename1));
}
}
}
return "main";
}
}
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
文件上传原理
文件上传的所有配置都被封装到了 MultipartAutoConfiguration
类里面了
文件上传所有的配置被封装到了 MultipartProperties.class
中
自动配置好了文件上传解析器 StandardServletMultipartResolver
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) // MULTIPART_RESOLVER_BEAN_NAME = multipartResolver
@ConditionalOnMissingBean(MultipartResolver.class) // 如果类中没有自定义配置的时候生效
// 文件上传解析器,只能上传标准的以 Servlet 方式上传的文件
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
首先找到 DispatcherServlet
的 doDispatch
方法中
boolean multipartRequestParsed = false;
记录一下文件上传是否已经被解析了
processedRequest = checkMultipart(request);
判断当前请求是不是一个文件上传请求,如果是把这个 request 包装,包装成一个 processedRequest。进去之后可以看到详情
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
// this.multipartResolver.isMultipart(request) 判断当前是不是文件上传请求,全系统只有一个
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
}
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}
进去 isMultipart
方法
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(),
// 判断上传是否是 multipart/
(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
}
因为上面的 multipart/ 判断,所以在上传文件的表单中必须写
默认情况下,Spring Boot 会提供 /error 处理所有错误的映射
对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息,对于浏览器客户端,响应一个 whitelabel 错误视图,以 HTML 格式呈现相同的数据。
要对其进行自定义,添加 View 解析为 error
要完全替换默认行为,可以实现 ErrorController
并注册该类型的 Bean 定义,或添加 ErrorAttributes
类型的组件以使用现有机制来替换其内容。
如果我们想要自定义错误页面,在 public 文件夹下或者 templates 文件夹下创建 error文件夹,在文件夹创建错误页面(4xx.html,5xx.html),这里的错误文件会被自动解析
自定义错误页面
ExceptionHandlerExceptionResolver
提供的处理支持/**
* 处理整个 web controller 的异常
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class,NullPointerException.class})// 处理异常
public String mathException(Exception e) {
log.info("异常{}",e);
return "login";// 处理异常后跳转的视图地址
}
}
底层是 ResponseStatusExceptionResolver
,把 responseStatus 注解的信息底层调用 response.sendError(statusCode,resolverReason); tomcat 发送的 /error
/**
* 自定义异常类,当 throw 抛出此异常的时候给出状态信息,异常信息
*/
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量太多")
public class UserTooManyException extends RuntimeException{
public UserTooManyException() {
}
public UserTooManyException(String message) {
super(message);
}
}
// Controller 中 模拟异常
@GetMapping("/form")
public String toFormPage() {
if (3 > 1) {
// 抛出异常
throw new UserTooManyException();
}
return "form/forms-upload";
}
抛出异常的时候会跳转到 404 页面给出提示信息 message
DefaultHandlerExceptionResolver
处理框架底层的异常
response.sendError(HttpServletResponse.SC_BAD_REQUEST,ex.getMessage());
@Order(value = Ordered.HIGHEST_PRECEDENCE) // 优先级,数字越小优先级越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
response.sendError(500,"我的错误信息");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
ErrorViewResolver 实现自定义处理异常
response.sendError error 请求就会转发给 Controller
你的异常没有任何人能处理。tomcat 底层 response.sendError error 请求就会转给 Controller
basicErrorController 要去的页面地址是 ErrorViewResolver
ErrorMvcAutoConfiguration
自动配置了异常处理规则
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
// 绑定了一些配置文件
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
@Bean
// 当容器中没有这个组件的时候生效,容器中放入的组件 ,类型 DefaultErrorAttributes id 为 errorAttributes
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Bean
// 容器中放入的组件,类型:BasicErrorController id:basicErrorController
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // 处理默认 /error 路径的请求
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);// 页面响应
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
要么响应页面,要么把 ResponseEntity 中的数据响应出去,相当于一个 json
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();
// 容器中还会有一个 view 组件,这个组件的id 叫做 error
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean
// 为了解析 view 视图,配置了一个 BeanNameViewResolver 的视图解析器
// 按照返回的视图名(error)作为组件的 id 去容器中找 View 对象
// 只要请求发到 /error 路径,就会找 error 视图,error 视图又是 View 中的一个组件,利用视图解析器找到 error 视图,最终 View 渲染的是什么样,页面就是什么样
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
如果想要返回页面,就会找 error 视图,默认是一个百页
// 写出去的是 JSON
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 错误视图、错误页
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
DefaultErrorViewResolverConfiguration
错误视图解析器组件
进去 DefaultErrorViewResolver
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
// 如果是客户端错误就是 4xx
views.put(Series.CLIENT_ERROR, "4xx");
// 如果是服务端错误就是 5xx
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
@Override
// 解析得到视图对象
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
// 如果发生错误,会以 HTTP 的状态码作为试图页面地址
// viewName 得到的其实是一个状态码,如果是 404错误就会找 error/404.html 的页面
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
DefaultErrorAttributes
定义了最终错误里面可以包含的那些内容public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 保存错误的默认属性 status trace exception ......
this.storeErrorAttributes(request, ex);
return null;
}
DispatcherServlet 的 doDispatcher 方法中可以看到
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
当异常被捕获之后进入视图解析流程(页面渲染流程)
mappedHandler:那个 Controller 处理器
mv:只有目标方法正确执行了才有值
dispatchException:目标方法中存在的异常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
// execption 保存的异常,如果异常不为空则执行下面代码
if (exception != null) {
// 判断异常是不是定义信息异常
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 处理 handler 的异常,处理结果保存为一个 mv(ModelAndView)
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
mv = processHandlerException
处理 handler 发生的异常,处理完成返回 mv(ModelAndView)
进去方法
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null; // 首先定义一个 ModelAndView
if (this.handlerExceptionResolvers != null) {
// 遍历所有的 HandlerExceptionResolver,查看谁能处理当前异常,处理器异常解析器
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
else if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
在上面的异常的自动配置的时候就放了一个 DefaultErrorAttributes
组件,其实就是一个 Handler 的异常处理器,专门处理异常
DefaultErrorAttributes
调用接口方法处理异常
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 保存 errorAttributes 错误的属性信息
this.storeErrorAttributes(request, ex);
// 返回 null
return null;
}
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
// 给 request 域中 ERROR_INTERNAL_ATTRIBUTE 属性
request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);
}
默认没有任何人能够处理异常,则异常会被抛出,如果没有任何能处理,则底层会发送 /error 请求
发送 /error 请求后会被底层的 BasicErrorController
进行处理
// 解析错误视图,包括错误的状态请求数据等信息
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
// 遍历所有的 ErrorViewResolver 查看谁能解析,如果能解析则封装 ModelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
默认只有一个 DefaultErrorViewResolver
,就是之前在ErrorMvcAutoConfiguration
中放入到组件
DefaultErrorViewResolver 作用就是把响应状态码作为错误页的地址拼接成 error/5xx.html,最终把模板引擎响应这个页面
‘//error/404.’
‘//error/404.html’
‘//error/4xx.’
‘//error/4xx.html’
如何在使用 Spring Boot 的过程中注入 web 的原生组件(Servlet、Filter、Listener)
官网:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications.embedded-container.servlets-filters-listeners
在之前 SpringMVC 要使用这些组件,需要把这些组件写好之后配置在 web.xml 文件中
When using an embedded container, automatic registration of classes annotated with @WebServlet
, @WebFilter
, and @WebListener
can be enabled by using @ServletComponentScan
.
编写一些 servelt ,然后在主启动类上使用注解 @ServletComponentScan
// 指定原生 Servlet 组件都放在哪里
@ServletComponentScan(basePackages = "com.thymeleaf")
@SpringBootApplication
public class ThymeleafApplication {
public static void main(String[] args) {
SpringApplication.run(ThymeleafApplication.class, args);
}
}
Servlet
// 直接响应,没有经过 Spring 的拦截器
@WebServlet(urlPatterns = "/myservlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("resources Servlet");
}
}
为什么自己写的 MyServlet 映射的路径直接相应,而不会经过 Spring 的拦截器?
从整个系统来看,一共有两个 Serlvet,
一个是自定义的 MyServlet,它要处理的路径是 /myservlet 路径
另一个是 DispatcherServlet,它处理的路径是 / 路径
扩展:DispatcherServlet 如何注册的
spring.mvc
ServletRegistrationBean
把 DispatcherServlet 配置进来DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath()); 这里的 getPath() 进去之后找到的是就是 / 路径
private String path = "/";
Filter
@Slf4j
@WebFilter(urlPatterns = {"/myservlet"})// 要拦截的 url 地址
public class MyFilter extends HttpFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
Listener
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到初始化项目完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目摧毁");
}
}
If convention-based mapping is not flexible enough, you can use the ServletRegistrationBean
, FilterRegistrationBean
, and ServletListenerRegistrationBean
classes for complete control.
@Configuration(proxyBeanMethods = true) // 保证依赖的组件始终是单实例的
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet() {
MyServlet myServlet = new MyServlet();
// 传入参数,1.自定刚才创建好的 MyServlet 类,2.访问的路径
return new ServletRegistrationBean(myServlet, "/myservlet", "/myservlet1");
}
@Bean
public FilterRegistrationBean myFilter() {
MyFilter myFilter = new MyFilter();
// 第一个参数是自定义的 MyFilter类,第二个参数是组件中的 myServlet,表示拦截的是 myServlet组件的访问路径
// return new FilterRegistrationBean(myFilter,myServlet());
// 拦截指定的路径
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/myservlet", "/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myServletListenerRegistration() {
MyServletContextListener myServletContextListener = new MyServletContextListener();
return new ServletListenerRegistrationBean(myServletContextListener);
}
}
Under the hood, Spring Boot uses a different type of ApplicationContext
for embedded servlet container support. The ServletWebServerApplicationContext
is a special type of WebApplicationContext
that bootstraps itself by searching for a single ServletWebServerFactory
bean. Usually a TomcatServletWebServerFactory
, JettyServletWebServerFactory
, or UndertowServletWebServerFactory
has been auto-configured.
Spring Boot 启动期间用了一个特殊的 IOC 容器(ServletWebServerApplicationContext),如果 Spring Boot 发现当前是一个 web 容器的话,IOC 容器就会变成ServletWebServerApplicationContext,这个容器在项目启动的时候会搜索 ServletWebServerFactory
(Servlet 的web 服务器工厂),
当Spring Boot 应用启动发现当前是 Web 应用,web 场景包-导入 tomcat
web 应用会创建一个 web 版的 IOC 容器(ServletWebServerApplicationContext)
Spring Boot 底层有很多的 Web 服务器工厂 TomcatServletWebServerFactory
, JettyServletWebServerFactory
, or UndertowServletWebServerFactory
底层会有一个自动配置类 ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration
导入了 ServletWebServerFactoryConfiguration
(工厂的配置类)
ServletWebServerFactoryConfiguration
根据动态判断系统中到底导入了那个 Web 服务器的包,(默认 web-starter 导入 tomcat 的包),进去会看到给容器中放了 TomcatServletWebServerFactory
, JettyServletWebServerFactory
, or UndertowServletWebServerFactory
TomcatServletWebServerFactory
最终创建出来 Tomcat 服务器,并启动
TomcatWebServer 的构造器有 初始化方法
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
initialize();
}
initialize() 方法中调用启动服务器
this.tomcat.start();
默认支持的 WebServer
Tomcat
,Jetty
,UnderTow
ServletWebServerApplicationContext
容器启动寻找 ServletWebServerFactory
并引导创建服务器
切换服务器
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-undertowartifactId>
dependency>
@EnableConfigurationProperties(ServerProperties.class)
public class ServletWebServerFactoryAutoConfiguration {
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
ServletWebServerFactory
进行绑定import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
server.xxx
ConfigurableServletWebServerFactory
@Bean
public ConfigurableServletWebServerFactory getConfigurableServletWebServerFactory () throws UnknownHostException {
TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
tomcatServletWebServerFactory.setPort(8088);
InetAddress address = InetAddress.getByName("127.0.0.1");
tomcatServletWebServerFactory.setAddress(address);
return tomcatServletWebServerFactory;
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@EnableWebMvc + WebMvcConfigurer ---- @Bean 可以全面接管 SpringMVC,所有规则全部自己重新配置;实现定制和扩展功能
原理:
WebMvcAutoConfiguration 默认的 SpringMVC的自动配置功能类。静态资源、欢迎页…
一旦使用 @EnableWebMvc,会 @Import(DelegatingWebMvcConfiguration.class)
DelegatingWebMvcConfigurer 的作用,只保证 SpringMVC 最基本的使用
WebMvcAutoConfiguration 里面的配置要能生效必须
@ConditionalOnMissingBean (WebMvcConfigurationSupport.class)
@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效
引入场景 starter – xxxxAutoConfiguration – 导入xxx组件 – 绑定xxxProperties — 绑定配置文件项
在我们使用过程中,第一步引入场景starter,然后绑定配置文件就可以使用了,中间的部分 Spring Boot 帮助我们处理了。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
从上面导入的内容我们可以看到,少了一个重要的内容,就是数据的驱动
因为它也不知道我们要使用什么数据库(MySQL,SQLServer,还是 Orcalc)
引入 mysql 驱动依赖,不需要写 version,因为 Spring Boot 已经对驱动的版本进行了仲裁
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
官方(默认)版本:
,需要注意我们自己的数据库版本要和默认的版本保持对应
方法1:依赖的时候引入具体的版本(maven 的就近依赖原则)
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.49version>
dependency>
方法2:官方里面定义版本在 properties
中
<properties>
<mysql.version>5.1.49mysql.version>
properties>
自动配置的类
DataSourceAutoConfiguration
数据源的自动配置的类
要想数据源的消息,只需要在配置文件中修改 spring.datasource
为前缀的东西
数据库连接池的配置,是自己容器中没有 DataSource 才自动配置的
底层配置好的连接池是:HikariDataSource
@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
事务管理器的自动配置
JdbcTemplateConfiguration
JdbcTemplate 的自动配置,可以操作数据库
@ConfigurationProperties(prefix = "spring.jdbc")
可以修改这个配置项来修改 JdbcTemplates等等…
修改配置项
spring:
datasource:
url: jdbc:mysql://localhost:3306/studentgrade?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
// 使用 Spirng Boot 给我们注册好的 JdbcTemplate m
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long count = jdbcTemplate.queryForObject("select count(*) from student", Long.class);
System.out.println(count);
}
平常的开发中 Druid 数据源也是非常受欢迎的,由于它有对数据源的整套的解决方案,数据源的全访问监控(防止 SQL 的注入等…)
druid 官方 github 地址 https://github.com/alibaba/druid
整合第三方技术的两种方式:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.17version>
dependency>
配置文件中可以配置的属性信息
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/studentgrade?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#SpringBoot默认是不注入这些的,需要自己绑定
#druid数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
#则导入log4j 依赖就行
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
@Configuration
public class MyDataSourceConfig {
// 默认的自动配置是判断容器中没有才会配置@ConditionalOnMissingBean(DataSource.class)
@Bean
@ConfigurationProperties("spring.datasource")// 绑定 application.yml中配置数据源的信息
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
// 开启内置监控功能
// 这里的 set 都可以写在 yml 配置文件中
druidDataSource.setFilters("stat");
return druidDataSource;
}
}
Druid 的内置监控功能
在自定义的数据源配置类MyDataSourceConfig
中注册监控的组件
/**
* 配置 druid 的监控页功能,这里配置好之后需要在 DataSource 组件中开启内置监控功能,上面代码中有(druidDataSource.setFilters("stat");)
* @return
*/
@Bean
public ServletRegistrationBean statViewServlet() {
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<StatViewServlet>(statViewServlet,"/druid/*");
// 设置查看的时候的用户名和密码
registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","111111");
return registrationBean;
}
然后启动程序后通过浏览器访问 localhost:8080/druid
就可以跳转到监控页面了
登录之后在 Session 监控中可以看到信息
开启 Web 应用
/**
* WebStatFilter 用于采集 web-jdbc 关联监控的数据
* @return
*/
@Bean
public FilterRegistrationBean WebStatFilter() {
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<WebStatFilter>(webStatFilter);
registrationBean.setUrlPatterns(Arrays.asList("/*"));
// 排除掉一些静态
registrationBean.addInitParameter("exclusions","*.js,*.jpg,*.gif,*.css,*.png,*.ico,/druid/*");
return registrationBean;
}
SQL防火墙
// 开启防火墙只需要在 DataSource 中配置就可以了druidDataSource.setFilters("stat,wall");
使用官方的场景启动器,上面的那些配置就不需要了
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.17version>
dependency>
@Configuration
@ConditionalOnClass(DruidDataSource.class)
// 如果 Spring 官方的数据源在前,则下面的 DataSource 就会不生效了
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
// 绑定的配置文件 在 `spring.datasource.druid` 下配置
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({
DruidSpringAopConfiguration.class,// 监控 Spring Bean,在`spring.datasource.druid.aop-patterns`下配置
DruidStatViewServletConfiguration.class,// 开启监控页的功能,在`spring.datasource.druid.stat-view-servlet.enabled`下配置,默认是开启的
DruidWebStatFilterConfiguration.class,// web 监控配置,默认开启的,在`spring.datasource.druid.web-stat-filter`下开启
DruidFilterConfiguration.class// 所有 Druid 自己 filter 的配置,这个会给容器中放入很多的组件,想要开启什么功能,这个里面都有配置的
})
public class DruidDataSourceAutoConfigure {
private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);
@Bean(initMethod = "init")
@ConditionalOnMissingBean
public DataSource dataSource() {
LOGGER.info("Init DruidDataSource");
return new DruidDataSourceWrapper();
}
}
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";
yml 配置文件,只是部分,详细查看druid 的官方文档,github地址上面有
spring:
datasource:
url: jdbc:mysql://localhost:3306/studentgrade?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
druid:
# 监控 Spring 这个包下的所有组件
aop-patterns: com.thymeleaf.*
filters: stat,wall,slf4j #底层开启功能,stat(sql监控),wall(防火墙),slf4j(日志记录)
stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin
login-password: 111111
# 禁用掉重置
reset-enable: false
web-stat-filter:
# 开启监控web 应用
enabled: true
url-pattern: /*
exclusions: '*.js,*.jpg,*.gif,*.css,*.png,*.ico,/druid/*'
filter:
stat: # 对上面 filters 里面的 stat 的详细配置
# 慢查询时间
slow-sql-millis: 1000
log-slow-sql: true
enabled: true
wall:
enabled: true
Mybatis 是第三方,所以 starter 是 mybatis-spring-boot-starter
github地址:https://github.com/mybatis
starter
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
之前使用 mybatis 的时候,需要有一个全局配置文件,创建一个 SqlSessionFactory
,然后通过SqlSession
找到 Mapper 接口来操作数据库,所有的东西都需要手动进行编写。
// 当引入了 mybatis 的 jar 包就不会生效了
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
// 当整个容器中只有一个候选的数据源生效
@ConditionalOnSingleCandidate(DataSource.class)
// 绑定配置文件
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {}
@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
可以在配置文件中修改 mybatis
开始的所有项来对mybatis 进行配置。
// 自动配置好了 SqlSessionFactory
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
// SqlSessionTemplate 里面组合了 SqlSession
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
// AutoConfiguredMapperScannerRegistrar 扫描配置文件都在那个位置,接口位置
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar
private BeanFactory beanFactory;
@Override
// AnnotationMetadata 拿到注解
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!AutoConfigurationPackages.has(this.beanFactory)) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
return;
}
// 找到所有标注了 @Mapper 注解的接口,只要我们写的操作Mybatis 的接口标注了 @Mapper 注解就会被自动扫描进来
logger.debug("Searching for mappers annotated with @Mapper");
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
使用
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase " value="true"/>
settings>
configuration>
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase " value="true"/>
settings>
configuration>
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
运行启动测试就可以了
这里省略了 实体类、服务类、控制层、方法接口(名字必须与 xxxxMapper.xml名字一样)的代码,这些写法与之前使用 Mybatis 方法一样。
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration: # 指定 mybatis 全局配置文件中的相关配置项,注意两个不能同时使用,要么使用 yml,要么创建xml文件yml指定位置
map-underscore-to-camel-case: true # 也可以在 yml 配置文件中设置属性
总结:
- 导入 mybatis 官方starter
- 编写 mapper 接口
- 编写 sql 映射文件并绑定 mapper 接口
- 在 application.yml 中指定 Mapper 配置文件的位置,以及指定全局配置文件的位置(建议不适用全局文件,直接使用yml 中的 mybatis 标签下写配置信息)
使用注解方式与之前使用 MyBatis 一样,不需要写 mapper 的映射文件,只需要在接口上使用注解即可
@Mapper
public interface CityMapper {
@Select("select * from city where id = #{id}")
City getById(Long id);
}
混合方式就是可以使用注解也可以使用接口映射文件来进行数据库的存储访问,简单的 SQL 语句可以使用注解方式操作;如果 SQL 语句比较麻烦,就可以使用接口映射文件xml 的方式进行操作。
有的的复杂的语句也可以使用 @Options 注解来完成
@Insert("insert into city (name,state,country) values(#{name},#{state},#{country})")
@Options(useGeneratedKeys = true,keyProperty = "id") // 设置自增的主键
Integer insertCity(City city);
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为了简化开发、提高效率而生
官方:https://mp.baomidou.com/
引入依赖
引入 Mybatis-Plus 的依赖后,之前的 jdbc 和 mybatis 的依赖都可以去掉,这个全部进行了封装
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
自动配置了什么
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class) // 底层用的是我们的默认的数据源
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
@ConfigurationProperties(
prefix = "mybatis-plus" // 配置项绑定,这块就是对 MyBatis-Plus 的绑定
)
public class MybatisPlusProperties { // 配置类
private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"}; // mapperLocations 自动配置好了,有默认值 classpath*:/mapper/**/*.xml 任意包的类路径下的所有 mapper 文件夹下任意路径下的所有 xml都是 SQL 映射文件
// SqlSessionFactory 核心组件配置好了
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
@Mapper 标注的接口也会被自动扫描
// 容器中也自动配置好了 SqlSessionTemplate
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
@MapperScan("com/thymeleaf/mapper")
,这样接口上不用再一个一个加 @Mapper 注解/**
* 只需要我们的 mapper 继承 BaseMapper 就可以进行 CRUD
*/
public interface UserMapper extends BaseMapper<User> {
}
继承的 BaseMapper 类中封装大量的操作方法
@Test
void testUserMapper() {
User user = userMapper.selectById(1);
System.out.println(user);
}
以 User 类为例
/**
* 只需要我们的 mapper 继承 BaseMapper 就可以进行 CRUD
*/
public interface UserMapper extends BaseMapper<User> {
}
/**
* extends IService 继承 MyBatis-Plus 中的接口,IService 是所有 Service 的接口
*/
public interface UserService extends IService<User> {
}
ef
3. 写 UserServiceImpl (UserService 的实现类),这两个有一定的联系,为了简化,接口继承了 IService 接口,实现类继承了 ServiceImpl 类
/**
* ServiceImpl
* UserMapper 表示的是要操作那个 Mapper
* User 返回数据的类型
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements com.thymeleaf.service.UserService {
}
@Autowired
UserService userService;
@GetMapping("/table-datatable")
public String dataTable(@RequestParam(value = "page", defaultValue = "1") Integer page, Model model) {
List<User> users = userService.list();
model.addAttribute("usersAll",users);
// 分页查询数据
Page<User> userPage = new Page<>(page, 2);
// 分页查询结果
Page<User> page1 = userService.page(userPage);
// 获得当前页的数据
model.addAttribute("page", page1);
return "table/tables-datatable";
}
<div class="card-body">
<div class="table-responsive">
<table id="example1" class="table table-bordered table-hover display">
<thead>
<tr>
<th>#th>
<th>idth>
<th>nameth>
<th>ageth>
<th>emailth>
<th>操作th>
tr>
thead>
<tbody>
<tr th:each="user,stat : ${page.records}">
<td th:text="${stat.count}">td>
<td th:text="${user.id}">Tiger Nixontd>
<td th:text="${user.name}">System Architecttd>
<td th:text="${user.age}">Edinburghtd>
<td th:text="${user.email}">61td>
<td><a th:href="@{/deleteUser/{id}(id=${user.id},page=${page.current})}">删除a>td>
tr>
tbody>
table>
div>
<div class="row-fluid">
<div class="span6">
<div class="dataTables_info" id="dynamic-table_info">
当前第 [[${page.current}]] 页 总计 [[${page.pages}]] 页 共 [[${page.total}]] 条记录
div>
div>
<div class="span6">
<div class="dataTables_paginate paging_bootstrap pagination">
<ul>
<li class="prev disabled"><a href="#">⬅ Previousa>li>
<li th:class="${num == page.current?'active':''}" th:each="num : ${#numbers.sequence(1,page.pages)}">
<a th:href="@{/table-datatable(page=${num})}">[[${num}]]a>li>
<li class="next"><a href="#">Next ➡a>li>
ul>
div>
div>
div>
div>
其他定义的接口与上面一样
/**
* RedirectAttributes 带数据进行重定向
**/
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id") Integer id,
@RequestParam(value = "page",defaultValue = "1") Integer page,
RedirectAttributes redirectAttributes) {
userService.removeById(id);
redirectAttributes.addAttribute("page",page);
return "redirect:/table-datatable";
}
<td><a th:href="@{/deleteUser/{id}(id=${user.id},page=${page.current})}">删除a>td>
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构,如字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets) 与范围查询,bitmaps,hyperloglogs 和 地理空间(geospatial) 索引半径查询。Redis 内置了复制(replication),LUA 脚本(Lua scripting),事务(transactions) 和不同级别的磁盘持久化(persistence),并通过 Redis哨兵(Sentinel) 和分区(Cluster) 提供高可用性(high availablity)。
使用的第一步引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)// 绑定配置文件
// 导入了 Lettuce 的客户端的连接配置,同时支持两个客户端操作 Redis
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {// 自动配置类
// 属性类 spring.redis 下面的所有配置是对 Redis 的配置
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
// 自动注入了 RedisTemplate,是以 K v 键值对的方式进行存储的,k 是Object ,v 是Object
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 还自动注入了一个 StringRedisTemplate ,这个认为 key 和 value 都是 String 的
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
在底层使用 RedisTemplate 和 StringRedisTemplate 就可以操作 Redis l
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class)
// 当spring.redis.client-type 客户端类型是 lettuce 的时候
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
// 给容器中放一个客户端的资源
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(ClientResources.class)
DefaultClientResources lettuceClientResources() {
// 客户端连接工厂,之后客户端获取的连接都是从这里获取的
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
LettuceConnectionFactory redisConnectionFactory(
默认使用的就是 Lettuce 客户端
启动 Redis 的服务
http://lss-coding.top/2021/09/19/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9%E6%80%BB%E7%BB%93/
在配置文件中配置 Redis 的访问地址和端口号
spring:
redis:
host: 192.168.43.219
port: 6379
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
void testRedis() {
ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
stringStringValueOperations.set("k2","v2");
String k1 = stringStringValueOperations.get("k2");
System.out.println(k1);
}
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
spring:
redis:
host: 192.168.43.219
port: 6379
client-type: jedis
jedis:
pool: # 对连接池的配置
max-active: 10
对每一个访问的 url 进行访问次数的统计,统计结果放到 Redis 中,存储的 key 的路径的名称,value 每一次访问 + 1
@Controller
public class RedisUrlCountInterceptor implements HandlerInterceptor {
@Autowired
StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
// 每次访问当前计数 rui+1
redisTemplate.opsForValue().increment(requestURI);
return true;
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* Filter 、Interceptor 区别
* Filter 是 Servlet 定义的原生组件,好处就是脱离的 Spring 也可以使用
* Interceptor 是 Spring 定义的接口,可以使用 Spring 的自动装配的功能
*/
@Autowired
RedisUrlCountInterceptor redisUrlCountInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(redisUrlCountInterceptor).addPathPatterns("/**")
.excludePathPatterns("/","/login","/css/**","/data/**","/font-awesome/**","images/**","/js/**","/lib/**","/plugins/**");
}
}
Filter 、Interceptor 区别
Filter 是 Servlet 定义的原生组件,好处就是脱离的 Spring 也可以使用
Interceptor 是 Spring 定义的接口,可以使用 Spring 的自动装配的功能
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
作为最新版本的 JUnit 框架,JUnit5 与之前版本的 JUnit 框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform:JUnit Platform 是在 JVM 上启动测试框架的基础,不仅支持 JUnit 自制的测试引擎,其他测试引擎也都可以接入
JUnit Jupiter:提供了 JUnit 5 的新的编程模型,是 JUnit 5 新特性的核心。内部包含了一个测试引擎,用于在 JUnit Platform 上运行
JUnit Vintage:由于 JUnit 已经发展很多年,为了照顾老的项目,JUnit Vintage 提供了兼容 JUnit4.x,JUnit3.x 的测试引擎
注意:Spring Boot 2.4 以上的版本移除了默认对 Vintage 的依赖。如果需要兼容 JUnit 4 需要自行引入
JUnit 5’s Vintage Engine Removed from spring-boot-starter-test
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.4-Release-Notes
2.4 版本移除了 JUnit 4 的兼容依赖 Vintage,如果想要继续兼容 JUnit4 的话需要自定引入依赖
<dependency>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.hamcrestgroupId>
<artifactId>hamcrest-coreartifactId>
exclusion>
exclusions>
dependency>
使用非常方便,只需要写一个测试类,在测试类上使用 @Test 注解就可以了
@SpringBootTest
class ThymeleafApplicationTests {
// 当我们创建一个 Spring Boot 项目后会自动给我们生成一个带有 @Test 的测试方法
@Test
void contextLoads() {
}
}
如果想要做单元测试只需要引入测试的启动器
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
需要使用 @Spring BootTest + @RunWith(SpringTest.class) 注解实现
Spring Boot 整合 JUnit 后使用非常的方便
@Test
注解(注意是 JUnit 5 版本的注解)官网列举了很多的注解:https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
@Test:表示方法是测试方法。但是与 JUnit 4 的@Test 不同,它的职责非常单一不能声明任何属性,扩展的测试将会由 Jupiter 提供额外测试
@ParameterizedTest:表示方法是参数化测试
@RepeatedTest:表示方法可重复执行
@RepeatedTest(5)
@Test
void testRepeated() {
System.out.println("测试Repeated");
}
@DisplayName("测试JUnit5功能测试类")
public class JUnit5Test {
@DisplayName("测试DisplayName注解")
@Test
void testDisplayName() {
System.out.println(1);
}
}
@BeforeEach
void testBeforeEach() {
System.out.println("测试就要开始了");
}
@AfterEach
void testAfterEach() {
System.out.println("测试结束了");
}
写两个 @Test 方法,点类上的运行按钮执行两个测试方法
@DisplayName("测试DisplayName注解")
@Test
void testDisplayName() {
System.out.println(1);
}
@Test
void test() {
System.out.println(2);
}
@BeforeAll
static void testBeforeAll() { // 需要定义为 static 方法,因为启动的时候就会调用这个方法
System.out.println("所有测试就要开始了");
}
与 @BeforeAll 一样:定义两个测试类,定义为static 方法
@AfterAll
static void testAfterAll() {
System.out.println("所有测试都结束了");
}
@Tag:表示单元测试类型,类似于 JUnit 4 中的 @Categories
@Disabled:表示测试类或者测试方法不执行,类似于 JUnit4 中的 @Ignore
@Disabled
@DisplayName("测试方法 2")
@Test
void test() {
System.out.println(2);
}
/**
* 方法的超时时间,如果超时抛出异常 TimeoutException
* @throws InterruptedException
*/
@Test
@Timeout(value = 5,unit = TimeUnit.MILLISECONDS)
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
在我们自定义的的测试类中没办法使用容器中组件
@Autowired
RedisTemplate redisTemplate;
@DisplayName("测试DisplayName注解")
@Test
void testDisplayName() {
System.out.println(redisTemplate);
System.out.println(1);
}
如果想要使用 容器中的组件,需要跟 Spring Boot 创建的时候自动创建的测试类一样,在类上加 @SpringTest
注解
@SpringBootTest
@DisplayName("测试JUnit5功能测试类")
public class JUnit5Test {
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class}) // Spring Boot 底层注解就有@ExtendWith注解,意思就是下面的测试都是使用 Spring的整个测试驱动进行测试
public @interface SpringBootTest {
断定某一件事情一定会发生,如果没发生就会认为它出了别的情况的错误。
断言是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions
的静态方法。JUnit 5 内置的断言可以分为下面的几种类别。
检查业务逻辑返回的数据是否合理,所有的测试运行结束以后,会有一个详细的测试报告(报告里面就会有那些方法成功,那些方法失败,失败的原因等等)
注意:如果有两个断言,第一个执行失败了第二个则不会执行
用来对单个值进行简单的验证
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
@DisplayName("测试简单断言")
@Test
void testSimpleAssertions() {
int add = add(2, 3);
// 判断相等,如果不相等给出错误信息 AssertionFailedError
assertEquals(6,add,"算数逻辑错误");
}
public int add(int num1, int num2) {
return num1 + num2;
}
@DisplayName("测试简单断言")
@Test
void testSimpleAssertions() {
int add = add(2, 3);
// 判断相等,如果不相等给出错误信息 AssertionFailedError
assertEquals(6,add,"算数逻辑错误");
Object ob1 = new Object();
Object ob2 = new Object();
assertSame(ob1,ob2);
}
@DisplayName("测试简单断言")
@Test
void testSimpleAssertions() {
assertSame(ob1,ob2,"两个对象不一样");
assertArrayEquals(new int[]{2,2},new int[]{1,2},"数组内容不相等");
}
assertAll 方法接受多个 org.junit.jupter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。
判断的时候当所有的断言执行成功才成功,否则失败
@DisplayName("组合断言")
@Test
void all() {
/**
* 断言所有成功才会往下走
*/
assertAll("test", () -> assertTrue(true && true,"结果不为true"),
() -> assertEquals(1, 2,"值不相等"));
}
在 JUnit 4 的时候,想要测试方法的异常情况时,需要用 @Rule 注解的 ExpectedException 变量还是比较麻烦的。而 JUnit 5 提供了一种新的断言方式 Assertions.assertThrows(),配合函数式编程就可以进行使用
@DisplayName("异常断言")
@Test
void testException() {
// 断定业务逻辑一定会出现异常
assertThrows(ArithmeticException.class, () -> {
int i = 10 / 2;
}, "为什么正常执行了,不应该有异常吗?");
}
Assertions.assertTimeout();为测试方法设置了超时时间
@DisplayName("超时断言")
@Test
void testAssertTimeout() {
// 如果测试方法超过 1 s 就会出现异常
Assertions.assertTimeout(Duration.ofMillis(1000),()->Thread.sleep(500));
}
通过 fail 方法直接使得测试失败
@DisplayName("快速失败")
@Test
void testFail() {
fail("测试失败");
}
JUnit5 中的前置条件类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法终止执行。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
assumeTrue 和 assumeFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足,测试执行并不会终止。
/**
* 测试前置条件
*/
@DisplayName("测试前置条件")
@Test
void testAssumptions() {
Assumptions.assumeTrue(true, "结果不是true");
System.out.println("执行成功");
}
JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制。
@DisplayName("嵌套测试")
public class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
// 嵌套测试下,外层的 test 不能驱动内层的 BeforeEach BeforeAll (After) 之类的方法提前或者之后运行
assertNull(stack);
}
@Nested // 表示嵌套测试
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
/**
* 内存的 test 可以驱动外层,外层的不能驱动内层的
*/
@Nested // 表示嵌套测试
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
参数化测试是 JUnit 5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。
利用 @ValueSource 等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要没新增一个参数就新增一个单元测试,省去了很多冗余代码。
@DisplayName("参数化测试")
@ParameterizedTest // 标注为参数化测试
@ValueSource(ints = {1,2,3,4,5})
void testParameterized(int i) {
System.out.println(i);
}
@NullSource:表示为参数化测试提供一个 null 的入参
@EnumSource:表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定 CSV文件内容作为参数化测试入参
@MethodSource:表示读取方法的返回值作为参数化测试入参(注意方法返回值需要是一个流)
@DisplayName("参数化测试")
@ParameterizedTest // 标注为参数化测试
@MethodSource("stringProvider")
void testParameterized2(String i) {
System.out.println(i);
}
// 方法返回 Stream
static Stream<String> stringProvider() {
return Stream.of("apple","banana");
}
如果参数化测试仅仅只能做到指定普通的入参还不是最厉害的,最强大之处的地方在于它可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现 ArgumentsProvider
接口,任何外部文件都可以作为它的入参。
在以后每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。Spring Boot 就抽取了 Actuator 场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
官网:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator
因为代码中 Redis 连接是失败的,所以访问 health 端口显示状态是 DOWN 掉的
访问路径 locahost:8080/actuator/health 中 actuator 后面的称为 Endpoints
官网上的 Endpoints 都是可以监控的指标,可以看到有很多的监控端点,但是这些端点默认不是全部开启的,除了 shutdown 这个端点外
# management 是所有 actuator 的配置
management:
endpoints:
enabled-by-default: true # 默认开启所有的监控端点
web:
exposure:
include: '*' # 以 web 方式暴露所有端点
最常用的 Endpoint
官网全部有罗列
https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
健康检查端点,一般用于云平台,平台会定时检查应用的健康状况,我们就需要 Health Endpoint 可以为平台返回当前应用的一系列组件健康状况的集合。
中的几点:
判断健康与否,需要取决于所有组件都是健康的才算健康,否则就是不健康。不健康就会提示 DOWN,健康提示 UP
management:
endpoints:
enabled-by-default: true # 默认开启所有的监控端点
web:
exposure:
include: '*' # 以 web 方式暴露所有端点
endpoint:
health:
show-details: always
提供详细的、层级的、空间指标信息,这些信息可以被 pull(主动推送)或者 push(被动获取)方式得到:
支持的暴露方式
如果在使用过程中开启所有指标的访问是非常危险的,所以有时候可以自定义开启某一个需要的指标
management:
endpoints:
enabled-by-default: false # 关闭所有监控端点 为 true 表示开启所有监控端点
web:
exposure:
include: '*' # 以 web 方式暴露所有端点
# 将某一个指标的 enabled 设置为 true
endpoint:
health:
show-details: always
enabled: true
info:
enabled: true
beans:
enabled: true
metrics:
enabled: true
@Component
public class MyHealthIndicator extends AbstractHealthIndicator {
// 真实的检查方法
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// 加入判断 MySQL 连接,在这里获取连接信息然后进行判断
// 保存在一些判断过程中信息
Map<String,Object> map = new HashMap<>();
if (1 == 1) {
// 健康
// builder.up();
builder.status(Status.UP);
map.put("msg","判断了10次");
}else{
// 不健康
// builder.down();
builder.status(Status.DOWN);
map.put("msg","判断了 0次");
}
builder.withDetail("code",100) // 返回的状态信息
.withDetails(map); // 返回携带的信息
}
}
info:
appName: thymeleaf
version: 1.0
mavenProjectVersion: @project.version@ # 得到 pom 文件中maven 的版本信息
@Component
public class AppInfo implements InfoContributor{
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("msg","你好")
.withDetail("hello","world");
}
}
想要判断某一个方法被访问了多少次
Counter counter; // 数量
public UserServiceImpl(MeterRegistry registry) {
counter = registry.counter("UserService.list.count");//UserService.list.count 自定义的指标的名称
}
@Override
public void TestMeterRegistry() {
counter.increment(); // 调用这个方法一次加 1
}
@Component
@Endpoint(id = "myservice")// 端点名
public class MyServiceEndpoint {
// 端点的读操作
@ReadOperation
public Map getDockerInfo() {
return Collections.singletonMap("dockerInfo","docker is running......");
}
@WriteOperation
public void stopDocker() {
System.out.println("Docker stopped......");
}
}
可以看到我们自定义的端点
拿到端点的值
https://github.com/codecentric/spring-boot-admin
使用手册:https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started
为了方便多环境适配,Spring Boot 简化了 profile 功能
在整个应用系统开发的时候可能会有一套数据库的配置信息,上线的时候又有另一套配置信息,如果每次开发上线都去修改配置文件会非常的麻烦,所以可以配置两套配置文件,一个上线的时候使用,一个平常开发测试的时候使用。
使用
创建两个配置文件:application-test.yml(测试环境),application-prod.yml(生产环境)
在配置文件中写入相同的配置
person:
name: test-张三
@RestController
public class HelloController {
@Value("${person.name:李四}") // 如果person.name 为空给默认值李四
private String name;
@GetMapping("/")
public String hello() {
return "Hello " + name;
}
}
如果没有指定配置环境,则默认的配置文件生效
如果想要指定是测试环境还是生成环境,则在默认的配置文件中进行指定
# 指定使用那个环境的配置文件,默认配置文件和指定环境的配置文件都会生效(如果默认配置文件和指定的环境配置中有相同的配置属性,则指定的会覆盖默认的)
spring.profiles.active=test
项目都是需要打包然后进行部署的,如果我们打包好了又需要切换环境重新打包非常的麻烦,所以我们可以在执行 jar 包的时候切换环境配置文件,命令行方式可以修改配置文件同时也可以修改配置文件中的内容信息
java -jar 打包好的项目 --spring.profile.active=prod --person.name=王五
官网:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles
模拟使用
有两个类,一个 Boss,一个Worker类,分别都包含属性 name、age,实现一个 Person 接口,配置文件 test 中配置了老板的信息,prod 配置了工人的信息,如果我们在 Controller 使用某一个对象的时候,自动注入 Person 接口,返回 person 的信息,不能确定生效的老板的类还是员工的类。所以在Boss 类和 Worker 类中分别进行指定配置文件的装配,@Profile
注解
// 激活配置文件,当 prod 文件的时候生效
@Profile("prod")
@Data
@Component
@ConfigurationProperties("person")
public class Worker implements Person{
private String name;
private Integer age;
}
// 激活配置文件,当 test 文件的时候生效// 激活配置文件,当 test 文件的时候生效
@Profile("test")
@Data
@Component
@ConfigurationProperties("person")
public class Boss implements Person{
private String name;
private Integer age;
}
#配置文件
# 默认配置文件 application.properties 进行激活使用的环境配置信息
spring.profiles.active=prod
# 测试环境配置文件 application-test.yml
person:
name: boss-张三
age: 30
# 生产环境配置文件 application-prod.yml
person:
name: worker-张三
age: 10
然后启动测试运行结果
启动后可以看到当前使用的生产 prod 环境,当然上面默认配置文件中是指定好的
返回值信息是 prod中配置好的内容
官网:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles.groups
# 激活一个组
spring.profiles.active=myprod
spring.profiles.group.myprod[0]=prod
spring.profiles.group.myprod[1]=dev
简单的说外部化配置就是抽取一些文件,放在外边集中管理
官网:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config
Spring Boot 可以让我们使用外部化配置,外部化设置可以在不修改代码的情况下可以适配多种环境,外部化来源包括:Java properties files, YAML files, environment variables, 和 command-line arguments.
jar 包当前目录
jar 包当前目录的config 目录
优先级:5 > 4 > 3 > 2 > 1
后面的可以覆盖前面的同名配置项,指定环境优先,外部优先
引入 starter — xxxxAutoConfiguration — 容器中放入组件 — 绑定 xxxxProperties — 配置项
customer-starter
hello-spring-boot-starter
作为我们的启动器hello-spring-boot-starter-autoconfigure
自动配置包创建好之后
hello-spring-boot-starter
的 pom.xml 文件中配置好自动配置类// 这个内容是自动配置类中pom.xml中的信息
<dependencies>
<dependency>
<groupId>com.examplegroupId>
<artifactId>hello-spring-boot-starter-autoconfigureartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
dependencies>
至此 hello-spring-boot-starter
配置好了,这个启动器只需要配置一个自动配置的信息
hello-spring-boot-starter-autoconfigure
自动配置类HelloProperties
用于保存配置类的配置属性信息// 指定前缀信息,在配置文件中使用这个 example.hello 就是配置我们自定义的starter 的属性
@ConfigurationProperties("example.hello")
public class HelloProperties {
private String prefix;
private String suffix;
// 省略 get/set 方法
}
HelloService
,真正执行的代码/**
* 默认不放到容器中
*/
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(String name) {
return helloProperties.getPrefix() + "你好" + name + helloProperties.getSuffix();
}
}
HelloServiceAutoConfiguration
@Configuration
@ConditionalOnMissingBean(HelloService.class)
@EnableConfigurationProperties(HelloProperties.class)// 默认把 HelloProperties 放到容器中
public class HelloServiceAutoConfiguration {
@Bean
public HelloService helloService() {
HelloService helloService = new HelloService();
return helloService;
}
}
/META-INF/spring.factories
,指定自动配置的类# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.auto.HelloServiceAutoConfiguration
至此启动器和自动配置类创建完成,然后将我们自己创建好了项目安装到 maven 中
使用
<dependency>
<groupId>org.examplegroupId>
<artifactId>hello-spring-boot-starterartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
- 配置属性信息
example.hello.suffix="111"
example.hello.prefix="222"
HelloService
类,执行方法@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping("/hello")
public String sayHello() {
String name = helloService.sayHello("张三三");
return name;
}
}
主启动类 debug 运行
// 这块是创建的流程 new SpringApplication(primarySources)
return run(new Class[]{primarySource}, args); // new 一个 Class 类
进去,1. 创建一个 Spring 应用,2. 然后调用 run 方法启动
return (new SpringApplication(primarySources)).run(args);
进去进去
@SuppressWarnings({ "unchecked", "rawtypes" })
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();
// 初始启动引导器,去 spring.factories 文件中找 Bootstrap类型的
this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
// 找 ApplicationContextInitializer 也是去 spring.factories 文件中找
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 找应用 监听器 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 决定哪一个是主程序类
this.mainApplicationClass = deduceMainApplicationClass();
}
private Class<?> deduceMainApplicationClass() {
try {
// 进到 堆栈中,找到有 main 方法的类就是主启动类
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
// 这块是 run 的流程 run(args)
// String... args 就是 main 方法中的参数 main(String[] args)
public ConfigurableApplicationContext run(String... args) {
// 停止的监听器,监控应用的启动停止
StopWatch stopWatch = new StopWatch();
// 记录的启动时间
stopWatch.start();
// 创建 引导上下文,并且获取到之前的 BootstrapRegistryInitializer 并且挨个执行 initializer() 方法 来完成对引导启动器上下文环境设置
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// 让当前应用进入 headless 模式(自力更生模式)
configureHeadlessProperty();
// 获取所有的 运行时监听器 并进行保存
SpringApplicationRunListeners listeners = getRunListeners(args);
// 遍历所有的 RunListener,调用 starting 方法,相当于通知所有感兴趣系统正在启动过程的正在starting,为了方便所有 Listener 进行事件感知
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 保存命令行参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 准备基础环境(保存环境变量等等),调用方法,如果有就返回,没有就创建基础一个,无论如何得有一个环境信息,配置环境环境变量信息,加载全系统的配置源的属性信息,绑定环境信息,监听器调用 environmentPrepard,通知所有的监听器当前环境准备完成
// prepareEnvironment 结束后所有环境信息准备完毕
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 配置一些忽略的 bean 信息
configureIgnoreBeanInfo(environment);
// 打印 Banner
Banner printedBanner = printBanner(environment);
// 创建 IOC 容器 就是创建 ApplicationContext,根据当前项目类型,servlet,AnnotationConfigServletWebServerApplicationContex,容器创建对象new 出来了
context = createApplicationContext();
// 记录当前应用的 startup事件
context.setApplicationStartup(this.applicationStartup);
// 准备 IOC 容器的信息 ,保存环境信息,后置处理流程,应用初始化器(遍历所有的 ApplicationContextInitlalizer,调用initialize 来对 IOC 容器进行初始化扩展功能),遍历所有的 listener 调用 contextPrepared,EvenPublishRunListener 通知所有的监听器 contextPrepared 完成
prepareContext(bootstrapContext, context, en vironment, listeners, applicationArguments, printedBanner);
// 刷新 IOC 容器,(实例化容器中的所有组件)
refreshContext(context);
// 刷新完成后工作,
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 所有监听器调用started 方法,通知所有监听器 started
listeners.started(context);
// 调用所有的 Runners,获取容器中 ApplicationRunner,CommandLineRunner,合并所有 Runner 按照 @Order 进行排序,遍历所有的 Runner,调用 run 方法,如果以上有异常调用 Listener 的faild 方法
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
// 如果以上都准备好并且没有异常,调用所有监听器的 running 方法,通知所有监听器进入 running 状态了,running 如果有错误继续通知 failed,通知监听器当前失败
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
// 如果一开始找到了 BootstrapRegistryInitializer 引导启动器就会调用这个方法,把每一个启动器遍历调用 initialize 方法
private DefaultBootstrapContext createBootstrapContext() {
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
return bootstrapContext;
}
@FunctionalInterface
public interface BootstrapRegistryInitializer {
/**
* Initialize the given {@link BootstrapRegistry} with any required registrations.
* @param registry the registry to initialize
*/
void initialize(BootstrapRegistry registry);
}
private SpringApplicationRunListeners getRunListeners(String[] args) {
// 拿到上下文 SpringApplication
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
// 去 spring.factories 中找 SpringApplicationRunListener.class
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
this.applicationStartup);
}
几个重要组件的自定义
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("MyApplicationContextInitializer...initialize");
}
}
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner...run...");
}
}
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
public SpringApplication application;
public MySpringApplicationRunListener(SpringApplication application,String[] args) {
this.application = application;
}
// 调用时机:应用刚一开始运行,刚创建好容器的基本信息的时候就调用 starting,相当于应用开始启动了
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MySpringApplicationRunListener...starting...");
}
// 环境准备完成的时候调用
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MySpringApplicationRunListener...environmentPrepared...");
}
// IOC 容器准备完成
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener...contextPrepared...");
}
// IOC 容器加载完成
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener...contextLoaded...");
}
// IOC 容器启动,调用 reflash 方法后
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener...started...");
}
// 整个容器创建完成全部都实例之后,整个容器没有异常都启动起来的时候调用
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener...running...");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MySpringApplicationRunListener...failed...");
}
}
@Component
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener...onApplicationEvent");
}
}
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner...run...");
}
}
然后再resources 文件夹下创建 /META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.lss.listener.MyApplicationContextInitializer
org.springframework.context.ApplicationListener=\
com.lss.listener.MyApplicationListener
org.springframework.boot.SpringApplicationRunListener=\
com.lss.listener.MySpringApplicationRunListener
执行查看输出结果就可以直到那个事件先执行了
**学习参考视频:**https://www.bilibili.com/video/BV19K4y1L7MT?p=1