优点:
缺点:
环境:java8以上,Maven3.3以上
我们使用版本:SpringBoot2.3.4
文档地址:https://docs.spring.io/spring-boot/docs/2.3.4.RELEASE/reference/html/
文档结构:
1.入门:
Getting Started
(Introducing Spring Boot, System Requirements, Servlet Containers, Installing Spring Boot, and Developing Your First Spring Boot Application)
Using Spring Boot
Build Systems, Structuring Your Code, Configuration, Spring Beans and Dependency Injection, DevTools, and more.
2.核心功能:
Spring Boot Features
Profiles, Logging, Security, Caching, Spring Integration, Testing, and more.
Spring Boot Actuator
Monitoring, Metrics, Auditing, and more.
Deploying Spring Boot Applications
Deploying to the Cloud, and Installing as a Unix application.
3.小技巧
“How-to” Guides
Application Development, Configuration, Embedded Servers, Data Access, and many more.
4.配置项文档
Application Properties
Common application properties that you can use to configure your application.
1.创建普通的maven项目
2.pom.xml
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
3.编写代码
MainApp.java
package com.sutong;
/**
* 主程序类
* 注意包结构,要将MainApp类放在最外侧,即包含所有子包,原因:spring-boot会自动加载启动类所在包下及其子包下的所有组件!
* SpringBootApplication注解: 告诉这是一个SpringBoot应用
*/
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class, args);
}
}
HelloController.java
package com.sutong.controller;
//@Controller
//@ResponseBody 下面所有方法都是带有ResponseBody
@RestController // 上面两个的复合注解
public class HelloController {
@RequestMapping("/hello")
public String sayHello() {
return "Hello, SpringBoot2";
}
}
4.测试
直接运行主程序类中的main方法,输入http://localhost:8080/hello
就可以看到响应的字符串!!!
5.简化配置
resources/application.properties
文件
# 就这一个统一配置文件!!都可以在文档Application Properties中找到可以配置什么
# server.port=8888 修改服务器端口号
6.简化部署
pom.xml
: 导入springboot
准备的插件,可以直接把项目打包成可执行的jar文件(直接在目标服务器执行即可)
打包点右上角的maven
的执行package
命令就行
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
cmd进入jar所在目录,
java -jar jar包名称
就可以直接启动!!(jar中包含了所有的环境依赖)
依赖管理
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
parent>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.3.4.RELEASEversion>
parent>
<properties>
<activemq.version>5.15.13activemq.version>
<mysql.version>8.0.21mysql.version>
....等等等
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.activemqgroupId>
<artifactId>activemq-amqpartifactId>
<version>${activemq.version}version>
dependency>
...等等等
dependencies>
dependencyManagement>
修改版本。例如修改mysql驱动(上面的默认8+版本,我们改为5+版本)
1、引入依赖默认都可以不写版本
<dependency> <groupId>mysqlgroupId> <artifactId>mysql-connector-javaartifactId> dependency>
2、引入非版本仲裁的jar,要写版本号。(学上面父项目的写法就行了,可以先查看底层规定的key和版本再改)
<properties> <mysql.version>5.1.25mysql.version> properties>
starter
场景启动器
(后面我们会见到很多这种:spring-boot-starter-*
)我们只要引入starter,这个场景所有常规需要的依赖我们都自动引入
spring-boot-starter-data-jdbc
spring-boot-starter-data-redis
springboot所有场景最基本的依赖
spring-boot-starter
,即自动配置的核心依赖。也可以使用第三方
*-spring-boot-starter
的 ,简化开发的场景启动器
自动配好Tomcat
web场景自动引入依赖
配置Tomcat
自动配好SpringMVC
web场景自动引入全套组件
自动配好SpringMVC常见组件(功能)
( 例如:dispatcherServlet,charactorEncodingFilter,视图解析器,multipartReesolver… )
默认包结构
主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来,无需以前的包扫描配置
想要改变扫描路径,主程序下@SpringBootApplication(scanBasePackages="com.sutong")
或者:@ComponentScan
指定扫描路径
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.sutong") // 默认是主程序所在的包以及以下
各种配置拥有默认值
MultipartProperties
按需加载所有自动配置项
spring-boot-autoconfigure
**包里面…
@Configuration + @Bean
配置类:
/**
* Configuration 告诉SpringBoot这是一个配置类,等同于配置文件
* 该注解里面Spring5.2后多了个属性proxyBeanMethods(是否是代理Bean的方法),默认true
* 如果是false的话,则不会生成代理对象
*
* Full (全配置) proxyBeanMethods = true 可以解决组件依赖问题,可以保证依赖的组件就是容器中的组件
* Lite (轻量级配置) proxyBeanMethods = false 不会检查方法返回的对象是否在容器中存在,就会快一点
* 一般组件之间不怎么依赖的话都调成false模式
*/
@Configuration(proxyBeanMethods = true)
public class MyConfig {
/**
* 给容器添加组件,以方法名作为组件的id,返回类型就是组件类型,
* 外部无论对这个注册方法调用多少次获取到都是IOC容器中发单实例对象(默认proxyBeanMethods = true)
* @return 返回对象就是组件在容器中保存的实例,默认单实例
*/
@Bean
public User getUser() {
return new User("Jack", 18);
}
}
主程序类:
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
// 返回的就是ioc容器
ConfigurableApplicationContext run = SpringApplication.run(MainApp.class, args);
User u1 = run.getBean("getUser", User.class);
User u2 = run.getBean("getUser", User.class);
System.out.println(u1 == u2); // true
MyConfig config = run.getBean(MyConfig.class); // 配置类也是个组件
// 如果proxyBeanMethods = true,
// 则获得的对象是MyConfig的代理对象,SpringBoot总会检查这个组件是否在容器中有,保持单实例,即下面输出true
// 如果proxyBeanMethods = false,则不生成代理对象则下面输出fasle,但上面还是输出true的
User user01 = config.getUser();
User user02 = config.getUser();
System.out.println(user01 == user02);
}
}
以前的注解也行
@Component
,@Controller
,@Service
,@Repository
@ComponentScan
指定包扫描规则
导入组件
@Import
写在组件的类上面(配置类也是一个组件)
@Import({User.class, DBHelper.class}) // 给容器中自动创建出这两个类型的组件,默认组件的名字就是全类名!!
@Configuration(proxyBeanMethods = true)
public class MyConfig {
}
条件装配
@Conditional
满足Conditional指定的条件,则进行组件注入
派生注解:
举例:
@Configuration(proxyBeanMethods = true)
public class MyConfig {
@Bean
public User getUser() {
return new User("Jack", 18);
}
// 有getUser这个组件名的话,才进行注册getPerson这个组件!!
// 也可以标注在类上面,对类下所有方法都生效
@ConditionalOnBean(name = "getUser")
public Person getPerson() {
return new Person("sutong");
}
}
@ImportResource
如果老公司还在用xml开发,里面有很多的bean,我们不用一个一个写成方法加上@Bean,只需要在一个配置类上面加上@ImportResource注解就行了
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="haha" class="com.sutong.bean.User">
<property name="name" value="Jack">property>
<property name="age" value="18">property>
bean>
beans>
@Configuration(proxyBeanMethods = true)
@ImportResource("classpath:bean.xml")
public class MyConfig {
}
application.properties
mycar.brand=BYD
mycar.price=100000
@Component // 只有在容器中的组件才会拥有SpringBoot提供的强大功能!!!
@ConfigurationProperties(prefix = "mycar") // prefix和配置文件中的那个前缀绑定
public class Car {
private String brand;
private Integer price;
// 下面有get/set/有参无参构造等
}
下面获取组件,就会有值了!!
@Configuration(proxyBeanMethods = true)
// 在配置类上面写,开启Car的属性配置功能!并把这个Car自动注册到容器中(这种第三方的类使用较多)
// 这个注册的组件的名字是: prefix的值-组件全类名,例如mycar-com.sutong.bean.Car
@EnableConfigurationProperties(Car.class)
public class MyConfig {
}
@ConfigurationProperties(prefix = "mycar") // 上面就是少了个@Component
public class Car {
private String brand;
private Integer price;
}
引导加载自动配置类 @SpringBootApplication
@SpringBootConfiguration
-> @Configuration代表当前是一个配置类
@ComponentScan
-> 指定扫描哪些
@EnableAutoConfiguration
@AutoConfigurationPackage
利用Registrar给容器中导入一系列组件(将主程序所在包下的所有组件导入进来)
@Import(AutoConfigurationImportSelector.class)
1、利用getAutoConfigurationEntry(..)给容器中批量导入一些组件
2、调用configurations = getCandidateConfigurations(..) 获取到所有需要导入到容器中的配置类
3、利用工厂加载得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。
默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories
这个文件里面写死了spring-boot一启动就要给容器中加载的所有配置类 -- 一共127个
按需开启自动配置项
虽然我们127个场景的所有自动配置启动的时候默认全部加载。即所有的xxxxAutoConfiguration
类,按照条件装配规则(@Conditional),最终会按需配置!!!
例如:@ConditionalOnClass({Advice.class})
修改默认配置
spring-boot-starter-web场景下
DispatcherServletAutoConfiguration.java
// 上面还有注册DispatcherServlet的方法,所以不用我们手动配置了 // 注册/规范MultipartResolver文件上传解析器 @Bean @ConditionalOnBean({MultipartResolver.class}) // 如果容器中有MultipartResolver类型的组件 @ConditionalOnMissingBean(name = {"multipartResolver"}) // 有这个类型但名字不是multipartResolver public MultipartResolver multipartResolver(MultipartResolver resolver) { // 给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。 // 直接把组件直接返回,并且把这个组件的名字改为multipartResolver,防止有些用户配置的文件上传解析器不符合规范 return resolver; } // 等等...
HttpEncodingAutoConfiguration.java
// 注册编码过滤器(解决请求编码问题) @Bean @ConditionalOnMissingBean // 如果我们没配置,就帮我们注册 public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); filter.setEncoding(...); filter.setForceRequestEncoding(..); filter.setForceResponseEncoding(..); return filter; } // ...等等
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先!!!即大量的@ConditionalOnMissingBean
SpringBoot先加载所有的自动配置类 xxxAutoConfiguration
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。都是在xxxxProperties
类里面拿,而xxxProperties
和配置文件进行了绑定
生效的配置类就会给容器中装配很多组件 (只要容器中有这些组件,相当于这些功能就有了)
定制化配置
SpringBoot自动配置原理流程:
xxxAutoConfiguration
-> 组件 -> xxxProperties
类里面拿值 -> application.properties
例如修改字符编码:直接在
application.properties
文件里面加上:
server.servlet.encoding.charset=GBK
引入场景依赖
查看自动配置了哪些(选做)
debug=true
开启自动配置报告。Negative(不生效的)\Positive(生效的)是否需要修改配置项
自己看源码(xxxxProperties
类绑定了配置文件的哪些前缀)
参照文档来修改配置项
自定义加入组件或者替换组件(用户配置优先)
自定义器 xxxCustomizer
类
1.依赖
2.安装IDEA插件
3.使用
SpringBoot帮我们也管理了
<lombok.version>1.18.12lombok.version>
就不用写版本号了
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
Person.java
@Data // 提供get、set、equals、hashCode、canEqual、toString方法
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private String name;
}
// 还有个 @Log4j/@Slf4j : 注在类上,提供对应的 Logger 对象,变量名为 log
热部署,项目里面改了内容,按ctrl+f9(重新编译,java类变化才真正重新编译,静态页面也不用)就能实时生效
就是个自动重启!!检查文件变化改变重启规则
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<optional>trueoptional>
dependency>
new project | Spring Initailizr | 按需要填写场景就行(我选择了WEB-Spring Web ,SQL-mybatis)
(版本选择好像只能选择最新的几个,我选的2.6.3,然后在改parent标签就行了)
1.自动导入一些依赖
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
3.自动编写好主配置类
@SpringBootApplication
public class Springboot02Application {
public static void main(String[] args) {
SpringApplication.run(Springboot02Application.class, args);
}
}
properities
同以前学习的写法一样
YAML
也是一种标记语言,非常适合用来以数据为中心的配置文件。(比xml更简洁,更轻量级)
语法:
k: v 中间有空格 ,大小写敏感
缩进表述层级关系(缩进的空格数不重要,相同层级左对其即可)
#表示注释
字符串不需要加引号,如果要加,‘ ’和" " 表示字符串内容,表示转义与不转义
如\n,单引号中作为字符串输出,双引号会换行(常规字符串加不加都一样)
数据类型:
字面量
对象
K: {k1:v2,k1:v2,k1:v2}
#或者
k:
k1: v2
k1: v2
k1: v2
数组
k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
例子:
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String userName;
// ....
}
person:
userName: zhangsan
boos: true
age: 18
birth: 2020/10/1 # 日期默认是'/'分割
hobby: # array List Set一样写法
- 篮球
- 足球
score: # Map写法
math: 90
english: 80
score: {math:90,english:80} # 或者这样
pet: # 对象
name: 小狗
weight: 30
allPets: # Map>
sick:
- 小米
- 小猫
health:
- 小熊
- 小鹿
application.properties
,application.yml
都存在都生效(properties优先)
配置提示
我们写的没有提示,加上下面的配置处理器就有了(记得重启一下)⭐
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
自动把驼峰命名法转化为带’-'的,都行!
这个jar包之和我们开发相关,SpringBoot建议我们在打包的时候不打包上面这个东西!!
(在以前的那个打包插件,加上configuration配置就行了)
<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>
Spring Boot 大多场景我们都无需自定义配置
Converter,GenericConverter,Formatter
HttpMessageConverters
(后来我们配合内容协商理解原理)MessageCodesResolver
(国际化用)(用的不多)index.html
页支持Favicon
ConfigurableWebBindingInitializer
,(DataBinder负责将请求数据绑定到JavaBean上)静态资源目录
只要静态资源放在类路径下: /static (或者/public 或者/resources 或者/META-INF/resources)
访问 : 当前项目根路径/ + 静态资源名
原理: 静态映射/**。(即拦截所有请求)
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
改变静态资源路径:
spring: resources: #其他默认的静态资源就访问不到了,haha文件夹里面才能访问到,如果要写更多,分割继续写就行 static-locations: [classpath:/haha/]
静态资源访问前缀
默认是无前缀的
spring:
mvc:
static-path-pattern: /res/**
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找 (这个后面用的挺多)
为了让拦截时能区分出静态资源和动态资源,所以规定静态资源前面加个前缀,拦截器在看到指定前缀时就放行,从而达到动态静态分开的目的。
webjar (用的不多)
webjar
即把js, css打包成jar(依赖)
自动映射webjar/**
<dependency>
<groupId>org.webjarsgroupId>
<artifactId>jqueryartifactId>
<version>3.5.1version>
dependency>
访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js
后面地址要按照依赖里面的包路径
静态资源路径下 index.html
配置这个访问前缀 static-path-pattern: /res/** 这个会导致welcome page功能失效!!
Controller
能处理"/index"
请求的
图片命名为 favicon.ico
放在静态资源目录下即可。(favicon
就是网站上面导航栏的小图标)
配置这个访问前缀 static-path-pattern: /res/** 这个会导致 Favicon 功能失效!!
因为浏览器会发送 /favicon.ico 请求获取到图标,整个session期间不再获取!
SpringMVC功能的自动配置类 WebMvcAutoConfiguration
,经分析是生效的
给容器中配了什么组件。
配置文件的相关属性和xxx进行了绑定。
WebMvcProperties == spring.mvc、ResourceProperties == spring.resources
public class WebMvcAutoConfiguration {
// 有 OrderedHiddenHttpMethodFilter
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
// 有一个有参构造器,参数全部从容器里面拿
// 资源处理的默认规则
public void addResourceHandlers(ResourceHandlerRegistry registry) {...}
}
public static class EnableWebMvcConfiguration ..{
// 配置欢迎页的方法
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(..) {...}
// WelcomePageHandlerMapping构造函数里面:
WelcomePageHandlerMapping(...) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
// 要用欢迎页功能,必须是/**,所以配置访问前缀欢迎页会失效!!
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 调用Controller /index
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
}
一些设置:
spring: mvc: static-path-pattern: /res/** resources: cache: period: 1000 # 1000 静态资源浏览器缓存1000秒 add-mappings: false # 禁用所有静态资源访问
Rest风格(使用HTTP请求方式动词来表示对资源的操作)
核心Filter
-> HiddenHttpMethodFilter
用法: 1.表单method=post,隐藏域 _method=put 2. SpringBoot中手动开启
spring:
mvc:
hiddenmethod:
filter:
enabled: true # 默认是false,true底层才会注册HiddenHttpMethodFilter组件
// 底层注册Filter的方法:
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter",
name = "enabled",
matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
如果使用Rest使用客户端工具,如
PostMan
直接发送Put、delete等方式的请求,无需Filter进行转化。
请求映射原理(了解)
SpringMVC功能分析都从 DispatcherServlet -> doDispatch()
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
总结:所有的请求映射都在HandlerMappings中。
SpringBoot自动配置欢迎页的
WelcomePageHandlerMapping
,访问 “/” 能访问到index.htmlSpringBoot自动配置了默认的
RequestMappingHandlerMapping
请求进来,遍历
handlerMappings集合
,挨个尝试所有的HandlerMapping
看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping,自定义 HandlerMapping
注解
@PathVariable
(路径变量)
@RequestHeadr
(请求头)
@RequestParam
(获取请求参数)
@RequestBody
(获取请求体值,POST才有)
@CookieValue
(获取Cookie值)
@RequestAttribute
(获取请求域中的数据)
@ModelAttribute
(不太常用,过了)
@Controller
public class TestController {
@RequestMapping("/car/{id}/owner/{username}")
@ResponseBody
public String getCar(@PathVariable("id") Integer id,
@PathVariable Map<String, String> pv,
// Map包装所有的,k-v必须都是String类型
@RequestHeader("User-Agent") String userAgent,
// 这个也可以标注Map,获取所有请求头信息
@RequestParam("hobby") String bobby,
// 获取k=v&k=v,也可以标注Map,所有请求参数都放入Map
@RequestBody String reqBody,
@CookieValue("_ga") String _ga,
// 也可以标注Map,所有的Cookie信息,也可标注整个Cookie,获取Cookie对象
@CookieValue("_ga") Cookie cookie) {
System.out.println("id:" + id);
System.out.println("id:" + pv.get("id"));
return "success";
}
@RequestMapping("/goto")
public String gotoPage(Model model) {
model.addAttribute("msg", "我是请求域里面的信息");
return "forward:/success"; // 转化到/success请求
}
@RequestMapping("/success")
@ResponseBody // @RequestAttribute获取请求域的数据,或者用原生的Servlet (一般都是页面去获取数据)
public String success(@RequestAttribute("msg") String msg) {
return msg;
}
}
@MatrixVariable
(矩阵变量)
/cars/{path}?k1=v1&k2=v2 这种写法称为queryString,查询字符串
/cars/path;k1=v1;k2=v2,v3,v4 这种称为 矩阵变量!!
如果页面禁用了Cookie,怎么获取Session中数据??
url重写:/abc;jsessionid=xxx 把cookie的值使用矩阵变量的方式进行传递,可以区分普通的请求参数
/boos/1/2 这种怎么加矩阵变量?
/boss/1;age=20/2;age=30 (分号前代表访问路径,后代表矩阵变量)
@Controller
public class TestController {
// /matrix/cars;low=34;brand=v2,v3,v4
// SpringBoot默认禁用了矩阵变量,需要手动开启
// 原理:对于路径的处理,底层用UrlPathHelper进行解析,
// 其中属性removeSemicolonContent(移除分号内容)支持矩阵变量的,默认是true
// 矩阵变量必须有url路径变量才能和解析!!!
@RequestMapping("/matrix/{path}")
@ResponseBody
public String testMatrix01(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path) {
System.out.println(path); // cars
return low + " " + brand.toString();
}
// /boss/1;age=20/2;age=30 矩阵变量重名情况
@RequestMapping("/boss/{bossId}/{empId}")
@ResponseBody
public String testMatrix02(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age", pathVar = "empId") Integer empAge) {
return bossAge + " " + empAge; // 20 30
}
}
// 开启矩阵变量(好像用的不多!!挺麻烦的)
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
// 可以自定义映射规则!!(或者使用@Bean注入一个WebMvcConfigurer)
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
// 重写addFormatter方法,自可以定义类型转化器 Converter,然后configurer.addConverter()
// ----------------------------------------------------------------------------
// 往容器放入WebMvcConfigurer组件也行,就不用实现WebMvcConfigurer接口了!!
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
}
上面的每个注解都对应一个方法参数解析器!!,xxxMethodArgumentResolver
Servlet API
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
底层使用 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)、
RedirectAttributes( 重定向携带数据)、
ServletResponse(response)、
Errors/BindingResult、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
自定义对象参数
可以自动类型转换与格式化,可以级联封装。
/**
* 姓名:
* 年龄:
* 生日:
* 宠物姓名:
* 宠物年龄:
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
使用ServletModelAttributeMethodProcessor 参数处理器进行解析
之间利用
WebDataBinder
中的124个Converter
类型转化器,进行类型转换,将请求的数据转化为我们JavaBean属性的类型。我们也可以自定义
Converter
使用jackson.jar + @ResponseBoby
引入web场景会自动引入jackson场景,自动返回json数据。
原理:是使用底层的各种返回值解析器进行处理
xxxReturnValueHandler, xxxMethodProcecssor
// 支持的的返回值类型,不止这些 ModelAndView Model View ResponseEntity ResponseBodyEmitter StreamingResponseBody HttpEntity HttpHeaders Callable DeferredResult ListenableFuture CompletionStage WebAsyncTask 方法标注 @ModelAttribute 且返回值是对象类型的 方法/类标注 @ResponseBody 注解,返回数据则使用 RequestResponseBodyMethodProcessor处理器处理!!!
标记@ResponseBody
的处理:
( 先RequestResponseBodyMethodProcessor
处理器处理,最后利用xxxHttpMessageConverter
消息转化器进行处理 )
将数据写为json流程:
内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
SpringMVC 会挨个遍历所有容器底层所有的 HttpMessageConverter
,看谁能处理
MappingJackson2HttpMessageConverte
可以将对象写为jsonMappingJackson2HttpMessageConverter
将对象转为json再写出去。处理返回值流程:先找到能处理对应返回值的处理器,然后再找具体的消息转化器。
内容协商:根据客户端接受能力不同,可能返回不同类型的媒体数据。
若客户端无法解析服务端返回的内容,即媒体类型未匹配,那么响应406
例如根据客户端接受能力返回json
或xml
(xml这个要引入依赖):
<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-xmlartifactId>
dependency>
看请求头中 Accept
(客户端能接受的类型):
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
可以看出application/xml
优先级高,如果服务器支持xml,则优先返回xml类型的媒体数据。(q=0.9)
而*/*
代表可以接受任意类型,包括application/json
(q=0.8)
可以使用
PostMan
软件,改变Accept请求头字段就能看到不同的效果!!这个就是SpringMVC底层内容协商的结果。
底层:先根据方法返回值以及注解, 找到符合的xxxHttpMessageConverter(服务器能力,可以多个,例如application/json,application/xml),然后按优先级遍历用户浏览器支持的所有类型(用户需要的),和服务器能产出的媒体类型去匹配(两个for循环,这个匹配可以说成是个最佳匹配),类型匹配成功就保存下来,然后最终选择一个最优的 媒体类型 进行转化。(第一个!?)
下面还有个匹配,再去找一下能处理这个 最优媒体类型 的消息转化器,最终用这个 消息转化器 进行转化对应的媒体类型,返回。
使用PostMan
软件可以轻易改变Accept
请求头,而用浏览器发送请求不能改变请求头,为了方便,可以开启浏览器参数方式内容协商功能(基于请求参数的内容协商):
spring:
mvc:
contentnegotiation:
favor-parameter: true # 开启请求参数的内容协商
如果xml,json都支持,则浏览器默认是xml,开启基于请求参数的内容协商后,加上format
参数就可以自定义需要的媒体类型了,
例如:http://localhost:8080/test/person?format=json
(format参数值底层只有xml,json两个值,我们可以在自定义)
http://localhost:8080/test/person?format=xml
Parameter策略(ParameterContentNegotiationStrategy)优先于Header策略(HeaderContentNegotiationStrategy)
总结:
实现多协议数据兼容。json、xml
0、标注
@ResponseBody
响应数据出去 调用 RequestResponseBodyMethodProcessor 处理1、
XxxProcessor
处理方法返回值。通过 XxxHttpMessageConverter 处理2、所有 HttpMessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter
例如:
[application/xml]
-> MappingJackson2XmlHttpMessageConverter
[application/json]
-> MappingJackson2HttpMessageConverter
[application/x-sutong]
-> XxxHttpMessageConverter
自定义:
// 自定义的 HttpMessageConverter,处理Person类型的响应数据格式!
public class SutongHttpMessageConverter implements HttpMessageConverter<Person> {
// 是否支持读,读:是否能解析请求来的数据
@Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
return false;
}
// 是否支持写,写:把Person对象是否转化为对应的协议数据
@Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return aClass.isAssignableFrom(Person.class);
}
// 底层有个重要的环节: 服务器要统计所有MessageConverter都能处理哪些内容类型
// 即application/x-sutong
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-sutong");
}
// 自定义的协议读入,我们上面禁用了
@Override
public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
// 自定义的协议写出
@Override
public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage)
throws IOException, HttpMessageNotWritableException {
String data = person.getUserName() + ";" + person.getAge() + "(我是自定义的)"; // 值之间用分号分割
// 写出去
OutputStream outputStream = httpOutputMessage.getBody();
outputStream.write(data.getBytes("GBK")); // 浏览器编码是GBK
}
}
添加:
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
// 添加我们自定义的消息处理器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new SutongHttpMessageConverter());
}
}
上面这样测试用
PostMan
发能行,即把请求头改为Accept:appplication/x-sutong浏览器中使用 参数的内容协商 不行!还要配置一些东西,即自定义参数策略(因为Parameter策略默认只支持xml和json)
自定义参数策略:
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
// 添加我们自定义的消息处理器..
// 自定义参数策略
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// 指定解析哪些参数对应的哪些配体内容
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json", MediaType.APPLICATION_JSON);
mediaTypes.put("xml", MediaType.APPLICATION_XML);
mediaTypes.put("x-sutong", MediaType.parseMediaType("application/x-sutong"));
// 需要传入一个Map,即参数映射,json -> application/json
ParameterContentNegotiationStrategy p = new ParameterContentNegotiationStrategy(mediaTypes);
// p.setParameterName("ff"); // 这里还可以改变“format”参数名
// 这个如果不添加默认的请求头策略就没了,就剩下我们自定义的参数协商策略了!
HeaderContentNegotiationStrategy h = new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(p, h));
}
}
注意:在将来我们需要自定义组件的时候,我们添加的功能会覆盖默认的很多功能!导致一些默认的一些功能失效,要小心点。
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染
原因:SpringBoot工程的打包结果是一个jar包,是压缩包,JSP不支持在压缩包中被编译运行,所以SpringBoot默认不支持JSP。
学习-模板引擎-Thymeleaf
是现代化、服务端Java模板引擎
表达式:
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值(和EL表达式差不多) |
选择变量 | *{…} | 获取上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接 |
片段表达式 | ~{…} | 类似jsp:include 作用,引入公共页面片段 |
字面量(’ '单引号表示字符串…),文本操作(+字符串拼接…),数学运算,布尔运算,比较运算,条件运算
设置属性值:
<input type="submit" value="Subscribe!" th:value="${subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
[[${session.user.name}]]
循环,判断:
<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}">tr>
<a th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">
view
a>
<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>
<a th:href="@{'/emp/'+${emp.id}}">ba>
Thymeleaf使用
Thymeleaf的场景依赖(引入starter
):
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
引入场景SpringBoot就自动配置好了Thymeleaf
,底层的 ThymeleafAutoConfiguration
:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
// 所有thymeleaf的配置值都在 ThymeleafProperties
// 配好了 SpringTemplateEngine
// 配好了 ThymeleafViewResolver
}
配置视图解析器的前后缀(有默认的)等:
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; // xxx.html
自定义:
spring:
thymeleaf:
prefix: classpath:/templates/table
suffix: .html
一般也不用自定义,跳转的时候使用默认的前缀,然后再加上中间的路径就行了 return "table/basic_table";
开发页面
开发页面前,页面上需要加上名称空间
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>successtitle>
head>
<body>
<h1 th:text="${msg}">哈哈h1>
<a th:href="@{/test/person}">链接a>
body>
html>
# 添加项目路径: server: servlet: context-path: /springboot02 # 项目路径,这样的话所有的请求必须在8080后面加上个 /springboot02
这样的话
@{/test/person}
会帮我们在前面自动加上项目路径! 只会针对绝对路径自动拼接
抽取公共部分
使用方法:
footer.html
<footer th:fragment="copy"> © 2011 The Good Thymes Virtual Grocery footer>
其他地方引入:
<body> ... 或者使用~{footer :: copy} <div th:insert="footer :: copy">div> <div th:replace ="footer :: copy">div> <div th:include="footer :: copy">div> body>
三种的区别(基本见名知意):
<body> ... <div> <footer> © 2011 The Good Thymes Virtual Grocery footer> div> <footer> © 2011 The Good Thymes Virtual Grocery footer> <div> © 2011 The Good Thymes Virtual Grocery div> body>
common.html
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common_header">
<meta charset="UTF-8">
<title>公共信息title>
<link/>
<link/>
head>
<body>
<h1 th:fragment="header_menu">头部导航h1>
<h1 th:fragment="footer">页脚h1>
<div id="common_script">
<script>script>
<script>script>
div>
body>
html>
success.html
DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>successtitle>
<div th:include="common :: common_header">div>
head>
<body>
<div th:replace="common :: header_menu">div>
<div th:replace="common :: footer">div>
<div th:replace="common :: #common_script">div>
body>
html>
(了解)
通过ViewNameMethodReturnValueHandler
处理返回的 视图名称字符串(视图地址)
处理目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer
里面,包括数据和视图地址
任何 目标方法执行完成 以后都会返回 ModelAndView
(数据和视图地址)
然后调用 processDispatchResult()
处理派发结果(页面该如何响应) -> render(mv, request, response) 页面渲染逻辑
首先 使用的所有视图解析器(xxxViewResolver
)尝试是否能根据当前目标方法返回值得到View对象
(View 对象,里面定义了页面的渲染逻辑)
遍历后,最终得到了通过Thymeleaf
创建RedirectView
对象,view = new RedirectView(..)
( 因为目标方法返回值是"redirect:/main"
)
ContentNegotiationViewResolver
内容协商解析器,里面包含了上面遍历的所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象
view.render(..)
视图对象调用自定义的render逻辑进行页面渲染工作,RedirectView
的render方法底层先获取目标url地址,再判断是否重定向携带数据,然后调用Servlet
的原生方法,即response.sendRedirect(encodedURL)
返回值以 forward: 开始: 底层会 view = new InternalResourceView(forwardUrl)
,最终调用Servlet原生转发方法:request.getRequestDispatcher(path).forward(request, response)
返回值以 redirect: 开始: view = new RedirectView()
render就是重定向,最终调用Servlet原生重定向方法:response.sendRedirect(encodedURL)
返回值是普通字符串: view = new ThymeleafView()
,之后调用Thymeleaf的模板引擎的process
方法进行页面渲染,然后用调用一大堆writer
输出html
有能力还可以自定义视图解析器+自定义视图
静态资源访问前缀:
spring:
mvc:
static-path-pattern: /static-res/** # 静态资源访问前缀
拦截器:
/**
* 登录检查:
* 1.配置要拦截的哪些请求
* 2.把拦截器配置放到容器中
*/
public class LoginInterceptor implements HandlerInterceptor {
// 目标方法执行前 - preHandle
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Object user = session.getAttribute("user"); // 因为登录成功后会把用户信息放到session域
if (user != null) {
return true; // 放行
}
// 拦截 -> 未登录 -> 跳转到登录页面(转发,重定向都行)
response.sendRedirect("/login");
return false;
}
// 目标方法执行后 - postHandle
// 视图渲染后 - afterCompletion
}
配置拦截器:
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/", "login", "/static-res/**"); // 放行主页和登录页面和静态资源
}
}
1、根据当前请求,找到HandlerExecutionChain(可以处理请求的handler以及handler的所有 拦截器)
2、先来 顺序执行 所有拦截器的 preHandle
方法
preHandler()
返回为true,则继续执行下一个拦截器的preHandle()
afterCompletion()
3、如果任何一个拦截器返回false。直接return
跳出不执行目标方法。
4、如果所有拦截器都返回True,执行目标方法
5、目标方法执行完后,倒序执行 所有拦截器的postHandle()
方法。
6、前面的步骤有任何异常都会直接 倒序触发 afterCompletion()
7、如果没有异常且没有被拦截,页面成功渲染完成以后,也会 倒序触发 所有拦截器的afterCompletion()
修改文件上传解析器的参数:
spring:
servlet:
multipart:
max-file-size: 10MB # 单个文件最大大小,默认1MB
max-request-size: 100MB # 整个请求的大小,默认10MB
页面:
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username"/> <br>
头像:<input type="file" name="header"/> <br/>
生活照:<input type="file" name="photos" multiple/> <br/>
<input type="submit" value="提交"/>
form>
控制器:
@Controller
public class FileController {
// 这个写的简陋,看详细的去看SpringMVC的笔记!!!!
@PostMapping("/upload")
public String upload(@RequestParam("username") String username,
@RequestPart("header") MultipartFile header,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
if (!header.isEmpty()) {
header.transferTo(new File("D:\\Learning\\" + header.getOriginalFilename()));
}
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
header.transferTo(new File("D:\\Learning\\" + header.getOriginalFilename()));
}
}
return "success";
}
}
文件上传自动配置类 -> MultipartAutoConfiguration
-> MultipartProperties
类里面 配置好了 StandardServletMultipartResolver
组件 (文件上传解析器)
@Bean(name = {"multipartResolver"})
@ConditionalOnMissingBean({MultipartResolver.class})
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
原理步骤:
请求进来使用文件上传解析器XxxMultipartResolver
使用 isMultipart
判断是否是多端数据,是则封装Request
(封装返回XxxMultipartHttpServletRequest
类型)
使用参数解析器 RequestPartMethodArgumentResolver
来解析请求中的文件内容封装成MultipartFile
将request文件信息封装为一个Map,MultiValueMap
把@RequestPart
的value,当作key(或者形参名),把Map中对应的值赋给形参就行了
最终transferTo()
里面使用FileCopyUtils
进行文件流的拷贝
默认情况下,Spring Boot提供/error
处理所有错误的映射
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息,
对于浏览器客户端,响应一个whitelabel
错误视图,以HTML格式呈现相同的数据
要对其进行自定义,添加 View
解析为 error
要完全替换默认行为,可以实现 ErrorController
并注册该类型的Bean
定义,或添加ErrorAttributes
类型的组件以使用现有机制但替换其内容。
static/error/
或者 templates/error/
下的404,5xx页面会被自动解析!
(如果命名为5xx所有的5开头的所有错误状态码就会跳转这个页面)
错误的JSON信息:
所以我们可以在错误页面显示这些信息:
DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>404title> head> <body> 页面找不到了 <h3 th:text="${message}">h3> <br> <p th:text="${trace}">p> body> html>
异常自动配置类 -> ErrorMvcAutoConfiguration
-> ServerProperties, WebMvcProperties
类里面自动配置了DefaultErrorAttributes组件
id:errorAttributes
@Bean
@ConditionalOnMissingBean(value = {ErrorAttributes.class}, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
// DefaultErrorAttributes实现了这两个接口!也是个异常解析器
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver {}
这个组件是往请求域放错误信息的。(定义了错误页面可以包含哪些数据)
看底层,发现key可以是:exception,trace,message,errors,timestamp,status,path…
类里面自动配置了BasicErrorController组件
id: basicErrorController
@Bean
@ConditionalOnMissingBean(value = {ErrorController.class}, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(...) {
return new BasicErrorController(...);
}
BasicErrorController
控制器里面默认处理默认 /error/**
路径的请求。(也可以修改配置: server.error.path: /error
)
这里面要不响应一个html ,即new ModelAndView("error"), model
,要不响应一个json,即new ResponseEntity(...)
响应html页面
@RequestMapping(produces = {"text/html"})
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
...
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
还会默认往容器放一个组件 View
组件 -> id: error(响应默认错误页)
@Bean(name = {"error"})
@ConditionalOnMissingBean(name = {"error"})
public View defaultErrorView() {
// defaultErrorView是个StaticView类型里面append了默认白页的错误页面的html,就是个SpringBoot默认错误页
return this.defaultErrorView;
}
为了解析这个视图还配置了一个视图解析器 BeanNameViewResolver
(视图解析器),按照返回的视图名(即上面的"error"视图名)作为组件的id去容器中找 View
对象。
响应json数据
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
...
return new ResponseEntity(body, status);
}
类里面自动配置了DefaultErrorViewResolver组件
(错误视图解析器) id: conventionErrorViewResolver
如果发生错误,会以HTTP的状态码作为视图页地址(即下面的形参viewName),加上 error/
前缀找到真正的页面,即error/404、5xx.html。这就解释了为什么我们把错误页面放到 static/error/
或者templates/error/
文件下会自动显示!!
// DefaultErrorViewResolver里面的方法:
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
...
return provider != null ? new ModelAndView(errorViewName, model)
: this.resolveResource(errorViewName, model);
}
总结:如果错误页面的可取值不够,就自定义DefaultErrorAttributes组件
如果想要修改错误跳转逻辑,修改默认的白页和json,就自定义BasicErrorController组件
如果不想把错误状态码对应的页面放到上面默认的文件夹下,就自定义DefaultErrorViewResolver组件
执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束。
并且用 dispatchException
封装异常,即用它来保存异常
进入视图解析流程(页面渲染…)当目标方法出现异常时,返回的ModelAndView为null,即下面的mv = null
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
处理目标方法发送的异常,重新为mv赋值
mv = processHandlerException(…)
XxxHandlerExceptionResolver
,看谁能处理当前异常(HandlerExceptionResolver处理器异常解析器)DefaultErrorAttributes
, ExceptionHandlerExceptionResolver
, ResponseStatusExceptionResolver
, DefaultHandlerExceptionResolver
ModelAndView
对象如果没有任何人能处理,最终底层就会再次发送 /error
请求(转发,这时请求域就已经保存了错误信息了),会被底层的**BasicErrorController
**控制器处理(浏览器则响应html)
XxxErrorViewResolver
看谁能解析。(默认就一个DefaultErrorViewResolver
)DefaultErrorViewResolver
,作用是把响应状态码作为错误页的地址,error/500.html
error/500.html
根据mv信息跳转对应的错误页面
自定义错误页
即 templates/error/404.html
,有精确的错误状态码页面就匹配精确,没有就找 4xx.html,都没有就触发白页
@ControllerAdvice + @ExceptionHandler
处理全局异常(以后开发推荐这种)
/**
* 处理整个WEB的Controller异常
*/
@ControllerAdvice
public class GlobalException {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String handlerArithAndNullException(Exception e) {
return "login"; // 视图地址
}
}
// 这种配置上面的默认异常解析器,就有可以处理的了,
// 即 ExceptionHandlerExceptionResolver!!!底层就不用重新发送/error请求了!!!
@ResponseStatus + 自定义异常
// 自定义异常 FORBIDDEN -> 403 出现就取状态码对应的页面
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {
public UserTooManyException() { }
public UserTooManyException(String message) {
super(message);
}
}
// 控制器
@Controller
public class ViewController {
@RequestMapping("/login")
public String login(User user) {
List<User> users = Arrays.asList(new User(), new User(), user);
if (users.size() > 2) {
throw new UserTooManyException();
}
return "success";
}
}
// 这种配置上面的默认异常解析器,就有可以处理的了,
// 即 ResponseStatusExceptionResolver!是把@ResponseStatus的信息保存到statusCode,resolvedReason,
// 底层再调用response.sendError(statusCode, resolvedReason),
// 即 tomcat发送的/error请求 (这次请求结束),BasicErrorController控制器进行处理,
// DefaultErrorViewResolver进行解析,状态码就是视图名称,跳转错误页面。(再处理不了取Tomcat的默认处理也,蓝白的那种)
Spring底层的异常,如 参数类型转换异常
这种的话则使用DefaultHandlerExceptionResolver
异常解析器进行处理!(处理SpringMVC底层的异常)
底层也会调用 response.sendError(statusCode, ex.getMessage())
, 即 tomcat发送的 /error
请求。
自定义实现 HandlerExceptionResolver
处理异常 (这个我们基本不自定义!)
// 自定义的异常解析器
// 我们配置的异常解析器,默认配置到最后一个,有时候轮不到我们自定义的处理就结束了,需要加上优先级,数越小优先级越高
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object o, Exception e) {
try {
response.sendError(511, "自定义的错误"); // 学底层的写法
} catch (IOException ex) {
ex.printStackTrace();
}
return new ModelAndView();
}
}
总结:DefaultErrorViewResolver 实现自定义处理异常;
- response.sendError(),error请求就会转给BasicErrorController
- 你的异常没有任何人能处理,tomcat底层 response.sendError,error请求就会转给BasicErrorController
- BasicErrorController 要去的页面地址是 DefaultErrorViewResolver 解析的
即(Servlet、Filter、Listener)
使用:@ServletComponentScan + @WebServlet
注解
原生Servlet类:
@WebServlet(urlPatterns = "/my") // Servlet3.0后提供的注解
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.getWriter().write("666");
}
}
主程序类:
@SpringBootApplication
// 指定原生Servlet放在的位置,不写这个原生的Servlet是不生效的
@ServletComponentScan(basePackages = "com.sutong.servlet")
public class Springboot02Application {
public static void main(String[] args) {
SpringApplication.run(Springboot02Application.class, args);
}
}
效果:直接响应,没有通过SpringMVC配置的拦截器
其他两个组件也一样,使用@WebFilter
,@WebListenr
,也要使用@ServletComponentScan
指定扫描 !!
ServletRegistrationBean
, FilterRegistrationBean
, and ServletListenerRegistrationBean
@Configuration // 配置类!配置Filter可能要依赖Servlet组件,所以可以不写 proxyBeanMethods = false
public class MyRegisterConfig {
@Bean
public ServletRegistrationBean myServlet() {
MyServlet servlet = new MyServlet();
// 传入Servlet,第二个参数是访问路径
return new ServletRegistrationBean(servlet, "/my");
}
@Bean
public FilterRegistrationBean myFilter() {
MyFilter filter = new MyFilter();
// 传入Filter,第二个参数是要拦截的Servlet对应的访问路径
// return new FilterRegistrationBean(filter, myServlet());
// 或者指定拦截的路径
FilterRegistrationBean frb = new FilterRegistrationBean(filter);
frb.setUrlPatterns(Arrays.asList("/test/**"));
return frb;
}
// Listener也一样!
}
效果和上面一样,直接响应,没有通过SpringMVC配置的拦截器
DispatchServlet 如何注册进来:(扩展)
容器中自动配置了
DispatcherServlet
属性绑定到WebMvcProperties
,对应的配置文件配置项是spring.mvc
通过
ServletRegistrationBean
把DispatcherServlet
配置进来。默认映射的是 “/” 路径。
没有通过SpringMVC配置的拦截器原因:
DispatcherServlet 处理请求 -> “/”
MyServlet 处理请求 -> “/my”
Tomcat-Servlet,如果多个多个Servlet都能处理到同一层路径,则精确优选原则!!则使用MyServlet 处理 “/my” 请求,则就不进入SpringMVC的流程了,则不经过 DispatcherServlet !!
这个章节可以实用性不高,了解一下不错
原理:
SpringBoot应用启动发现当前是Web应用(导入了Web场景 -> 默认导入tomcat)
Web应用会创建一个Web版的ioc容器 ServletWebServerApplicationContext
,启动的时候回去找,ServletWebServerFactory
(WebServer工厂,即Servlet的Web服务器)
SpringBoot底层默认有很多的WebServer工厂:TomcatServletWebServerFactory
, JettyServletWebServerFactory
, or UndertowServletWebServerFactory
上面这些工厂也不用我们配置,底层直接会有一个自动配置类,即ServletWebServerFactoryAutoConfiguration
,这个绑定了ServerProperties
属性类,这个自动配置类导入了 ServletWebServerFactoryConfiguration
配置类,这个配置类里面选择的配置了上面的三个WebServer工厂(即动态判断系统中到底导入的那个Web的服务器的包,默认是web-starter导入tomcat包,则容器中就有 TomcatServletWebServerFactory)
TomcatServletWebServerFactory
创建出Tomcat服务器并启动。
Tomcat服务器TomcatWebServer
的构造器拥有初始化方法initialize()
-> this.tomcat.start()
启动
所谓内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
切换服务器,即导入对应的starter就行了,默认是tomcat的服务器,即
spring-boot-starter-tomcat
(
spring-boot-starter-web
里面默认自动导入了spring-boot-starter-tomcat
)其他还有:
spring-boot-starter-undertow
,spring-boot-starter-jetty
…<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>
定制Servlet容器:
修改配置文件 server.xxx
实现 WebServerFactoryCustomizer
把配置文件的值和 ServletWebServerFactory
进行绑定。
一般 xxxxxCustomizer
都是一些 定制化器,可以改变xxxx的默认规则。
直接自定义 ConfigurableServletWebServerFactory
定制化的常见方式 :
修改配置文件
实现一些XxxCustomizer
定制化器
编写自定义的配置类 : XxxConfiguration + @Bean
替换/增加容器中默认组件
web应用,@Configuration
+ 实现WebMvcConfigurer
,即可完成定制化web功能
@EnableWebMvc + @Configuration + WebMvcConfigurer
(@Bean) 可以全面接管SpringMVC
底层几乎所有组件失效(WebMvcAutoConfiguration
自动配置类失效),例如里面配置的欢迎页,视图解析器,静态资源,Rest风格的过滤器…都失效,但会保证一些最基本的功能生效,几乎所有规则全部自己重新配置, 实现定制和扩展功能(慎用)
场景starter —— xxxxAutoConfiguration —— 导入xxx组件 —— 绑定xxxProperties —— 绑定配置文件项
导入JDBC场景
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>
SpringBoot并没有帮我们导入驱动,因为官方不知道我们接下要操作什么数据库,(数据库版本和驱动版本对应)
<properties>
<mysql.version>5.1.25mysql.version>
properties>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
dependencies>
修改配置项:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis
username: root
password: 9527
type: com.zaxxer.hikari.HikariDataSource # 默认就是这个
driver-class-name: com.mysql.jdbc.Driver
jdbc:
template:
query-timeout: 3
数据源的自动配置 DataSourceAutoConfiguration
绑定了的DataSourceProperties
属性类,即对应spring.datasource
开头的配置
数据库连接池的配置,是自己容器中没有DataSource才自动配置的
@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() {
protected PooledDataSourceConfiguration() { }
}
底层配置好的连接池是:HikariDataSource (具体配置在 DataSourceConfiguration类里面),这个数据源在市面上还算优
事务管理器的自动配置DataSourceTransactionManagerAutoConfiguration
JdbcTemplate组件的自动配置 JdbcTemplateAutoConfiguration
spring.jdbc
来修改 JdbcTemplate 相关属性Jndi的自动配置JndiDataSourceAutoConfiguration
分布式事务相关的自动配置XADataSourceAutoConfiguration
官方文档:https://github.com/alibaba/druid
(自带监控功能,一些企业还是喜欢用Druid数据源)
整合第三方技术的两种方式,1.自定义 2.找starter
自定义
依赖:
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.8version>
dependency>
配置数据源和监控页:
@Configuration(proxyBeanMethods = false)
public class MyDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() throws SQLException {
// 按理说要设置url,username..等但在配置文件中已经写过了,直接绑定就行了
DruidDataSource dataSource = new DruidDataSource();
dataSource.setFilters("stat,wall"); // 开启SQL监控和SQL防火墙功能!!这个也能在配置文件里面写!!
return dataSource;
}
// 配置druid监控页功能,这个是个标准的Servlet组件
@Bean
public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
StatViewServlet servlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> srb =
new ServletRegistrationBean<>(servlet, "/druid/*");
// 添加一些初始化参数,访问监控页的密码,ip等..
HashMap<String, String> initParameters = new HashMap<>();
initParameters.put("loginUsername", "admin");
initParameters.put("loginPassword", "123456");
srb.setInitParameters(initParameters);
return srb;
}
// 配置 WebStatFilter用于web-jdbc监控的数据
@Bean
public FilterRegistrationBean<WebStatFilter> webStatFilter() {
WebStatFilter filter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> frb = new FilterRegistrationBean<>(filter);
frb.setUrlPatterns(Arrays.asList("/*"));
HashMap<String, String> initParameters = new HashMap<>();
// 不监控一些静态资源
initParameters.put("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
frb.setInitParameters(initParameters);
return frb;
}
}
还有很多监控Spring监控,Session监控等…
starter方式
导入starter:(上面这么多的东西就不用我们写了)
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.2.8version>
dependency>
分析自动配置:
DruidDataSourceAutoConfigure
-> DruidStatProperties,DataSourceProperties
绑定了这两个配置spring.datasource.druid, spring.datasource
类上面使用@Import
导入了下面的四个配置类:
DruidSpringAopConfiguration
配置了监控Spring中组件的的相关东西,配置项spring.datasource.druid.aop-patterns
DruidStatViewServletConfiguration
开启Servlet监控页的功能,配置项spring.datasource.druid.stat-view-servlet
,可设置访问监控页的密码等…
DruidWebStatFilterConfiguration
配置WebStatFilter,配置项spring.datasource.druid.web-stat-filter
DruidFilterConfiguration
配置了其他一些Filter,有下面这么多,绑定了下面的这些配置项
spring.datasource.druid.filter.stat
spring.datasource.druid.filter.config
spring.datasource.druid.filter.encoding
spring.datasource.druid.filter.slf4j
spring.datasource.druid.filter.log4j
spring.datasource.druid.filter.log4j2
spring.datasource.druid.filter.commons-log
spring.datasource.druid.filter.wall
spring.datasource.druid.filter.wall.config
编写配置项:(示例)更多看官方文档!!
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis
username: root
password: 9527
type: com.alibaba.druid.pool.DruidDataSource # 这个可以不写,底层会根据导入依赖选择注入
driver-class-name: com.mysql.jdbc.Driver
druid:
stat-view-servlet: # 配置监控页功能
enabled: true # 开启,默认是false
login-username: admin
login-password: 123456
reset-enable: false # 禁用重置按钮
web-stat-filter: # 监控web,Web应用和URL监控
enabled: true
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
# 开启多个过滤器,即Sql监控和Sql防火墙(可以使用filter配置单个过滤器的详细信息)
filters: stat,wall
filter:
stat:
enabled: true # 默认true
slow-sql-millis: 1000 # 规定是慢查询的毫秒时间
log-slow-sql: true # 日志记录下来慢查询
wall:
enabled: true # 默认false
config:
update-allow: false # 所有的数据库更新都会被防火墙拦截
delete-allow: false # 不允许删数据
drop-table-allow: false # 不允许删表
aop-patterns: com.sutong.* # 这个包下的所有组件都监控,监控SpringBean
官方地址:https://github.com/mybatis
starter:
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.3version>
dependency>
要求:
- 2.2.x: MyBatis 3.5+, MyBatis-Spring 2.0+(推荐2.0.6+), Java 8+ 和 Spring Boot 2.5+
- 2.1.x : MyBatis 3.5+, MyBatis-Spring 2.0+(推荐2.0.6+), Java 8+ 和 Spring Boot 2.1-2.4
分析自动配置
MybatisAutoConfiguration
-> MybatisProperties
属性类 -> 配置项mybatis
导入了SqlSessionFactoryBean
, 自动配置好了,数据源就是用的容器中的数据源
导入了SqlSessionTemplate
,这个其实就个SqlSession
(组合了)
mybatis:
executor-type: batch # 底层就会创建一个批量的SqlSessionTemplate(就是sqlSession)
导入了@Import({AutoConfiguredMapperScannerRegistrar.class})
只要我们写的操作MyBatis标注了@Mapper
注解就会被自动扫描进来!!
或者在SpringBoot的启动类上面加上@MapperScan
指定,就不用每个Mapper都写@Mapper
注解了
使用
配置:
全局配置文件mybatis/mybatis-config.xml
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>
application.yml
mybatis:
config-location: classpath:mybatis/mybatis-config.xml # 指定Mybatis全局配置文件的位置
mapper-locations: classpath:mybatis/mapper/*.xml # 指定XxxMapper.xml的文件位置
# 上面全局配置文件中的配置也能在这里面写,前缀:mybatis.configuration !!!!(推荐这种)
# 注意全局配置文件在这里面写的话,config-location就不要配置了,否则底层也不知道以那个配置项为准了!!
接口,com.sutong.dao/EmpMapper.java
:
@Mapper
public interface EmpMapper {
Emp getEmpById(Integer id);
}
sql映射文件,mybatis/mapper/EmpMapper.xml
:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sutong.dao.EmpMapper">
<select id="getEmpById" resultType="com.sutong.bean.Emp">
select * from t_emp where id = #{id}
select>
mapper>
在Service层直接注入Mapper就能直接使用了!!
纯注解(长SQL就不建议了)(还可以注解和xml映射文件混合使用,但不建议…)
@Mapper
public interface EmpMapper {
@Select("select * from t_emp where id = #{id}")
Emp getEmpById(Integer id);
@Insert("..")
@Options(useGeneratedKeys = true, keyProperty = "id") // Insert标签的设置项
void saveEmp(Emp emp);
}
最佳实践:
引入mybatis-starter
配置application.yaml中,指定mapper-location位置即可
编写Mapper接口并标注@Mapper注解
简单方法直接注解方式
复杂方法编写mapper.xml进行绑定映射
@MapperScan(“com.sutong.dao”) 简化,其他的接口就可以不用标注@Mapper注解
引入starter
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
自动配置类:
MybatisPlusAutoConfiguration
-> MybatisPlusProperties
-> 前缀mybatis-plus
mapperLocations
属性有默认值(Mapper映射文件的位置),classpath*:/mapper/**/*.xml
SqlSessionFactory
自动配置好了,数据源用的是容器中的数据源
SqlSessionTemplate
自动配置了
@Mapper
标注的接口也会被自动扫描,建议批量扫描@MapperScan
CRUD
Mapper接口:
// BaseMapper是MyBatisPlus提供的(里面对单表的增删查改方法),泛型是要操作的实体类的类型
public interface UserMapper extends BaseMapper<User> { }
Service层直接使用就行!
启动器:
SpringBoot1.x使用的是Jedis
操作redis,SpringBoot2.x默认使用lettuce
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
自动配置:
RedisAutoConfiguration
自动配置类 -> RedisProperties
属性类 -> spring.redis
配置项
连接工厂是准备好的:LettuceConnectionConfiguration(默认这个生效)、JedisConnectionConfiguration
自动注入了RedisTemplate
(k:v都是Object)
自动注入了StringRedisTemplate
(k:v都是String)
底层只要我们使用StringRedisTemplate、RedisTemplate就可以操作redis
Linux端配置好Redis就可以连接了
配置文件:
spring:
redis:
#url: redis://user:[email protected]:6379 (包含下面的三,user将会忽略)
database: 0 # 数据库索引,默认为0
host: 192.168.200.xxx # Redis服务器地址,默认localhost
port: 6379 # 端口,默认6379
password: 9527 # 默认是空
########################################### 其他的一些配置:
spring:
redis:
# ...
timeout: 1800000 #连接超时时间(毫秒)
lettuce:
pool: #写这个配置需要:commons-pool2 依赖(选择性引入,SpringBoot也管理了版本)
max-active: 8 #最大阻塞等待时间(负数表示没限制,默认8)
max-idle: 8 #连接池中的最大空闲连接,默认8
min-idle: 0 #连接池中的最小空闲连接,默认0
测试:
@SpringBootTest
public class RedisTest {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void test01() {
System.out.println(redisTemplate);
System.out.println(stringRedisTemplate);
// k,v都是String的客户端
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.set("k1", "HelloWord");
System.out.println(ops.get("k1"));
}
}
StringRedisTemplate
继承了RedisTemplate
,两种有相似之处,也有不同。
区别:
两者的数据是不共通的。
使用的序列化类不同:
StringRedisTemplate
使用的是StringRedisSerializer
:
采用的是把字符串本身转化位字节数组,和对应编码下字节数组之间的转换,(默认UTF-8编码)。
数据值是以可读的形式显示,即保存的什么,在客户端看到的就是什么
RedisTemplate
使用的是JdkSerializationRedisSerializer
:
存入数据会将数据先序列化成字节数组(使用ByteArrayOutputStream
)然后在存入Redis数据库(使用OutputStream
)。
数据不是以可读的形式展现的,而是以字节数组显示。
使用时注意事项:
当你的redis数据库里面本来存的就是字符串,或要取的数据就是字符串类型时,那么你就使用StringRedisTemplate
即可。但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate
是更好的选择。
切换Jedis:
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-data-redisartifactId> <exclusions> <exclusion> <groupId>io.lettucegroupId> <artifactId>lettuce-coreartifactId> exclusion> exclusions> dependency> <dependency> <groupId>redis.clientsgroupId> <artifactId>jedisartifactId> dependency>
配置:
spring: redis: database: 0 host: 192.168.200.xxx port: 6379 password: 9527 client-type: jedis # 可指定
lettuce
与jedis
两者区别:
lettuce:底层是用netty实现,线程安全,默认只有一个实例
jedis:可直连redis服务端,配合连接池使用,可增加物理连接
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库
JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: 是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter: 提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。
JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。
starter, 场景:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
SpringBoot 2.4+ 版本中的
spring-boot-starter-test
场景移除了默认对Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能,例如org.junit.@Test)
SpringBoot整合了Junit以后,测试非常方便(不用再测试类上面标注好几个注解了)
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
@SpringBootTest
class Springboot02ApplicationTests {
// 如果需要组件,只需要@Autowired就行了,Junit的类具有Spring的功能
// 测试方法上加上@Transactional,测试完成后会自动回滚!!
@Test
void contextLoads() {
System.out.println("haha");
}
}
@Test:表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
@RepeatedTest:重复测试,value表示次数
@DisplayName :为测试类或者测试方法设置展示名称
@BeforeEach :表示在每个单元测试之前执行
@AfterEach:表示在每个单元测试之后执行
@BeforeAll :表示在所有单元测试之前执行,一般是静态方法或者标注上@TestInstance
,AfterAll也一样
@AfterAll:表示在所有单元测试之后执行
@Tag :表示单元测试类别,类似于JUnit4中的@Categories
@Disabled:表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
@Timeout:表示测试方法运行如果超过了指定时间将会抛出异常,单位是可以自己定
@ExtendWith:为测试类或测试方法提供扩展类引用
如果想要使用Spring组件需要在测试类上面标上@SpringBootTest
,而这个里面包含了:
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
@ParameterizedTest:表示方法是参数化测试,下方会有详细介绍
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions
的静态方法。JUnit 5 内置的断言可以分成以下几个类别
用来:检查业务逻辑返回的数据是否合理(我们就不要print然后人眼判断了)。所有的测试运行结束以后,会有一个详细的测试报告(点右上角Maven的test指令,就会跑一次全部的单元测试,生成报告)
简单断言
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
import static org.junit.jupiter.api.Assertions.*; // 这样导入的话就不用写前面那个Assertions了!简单点
@Test
public void testAssertions() {
// 前面是你期望值,后面是真实的值,还可以传入第三个参数是断言失败的提示信息
Assertions.assertEquals(2, sum(1, 1));
}
// 如果前面的断言失败了,后面的代码不会执行
数组断言
通过 assertArrayEquals
方法来判断两个对象或原始类型的数组是否相等(逻辑相等即equals,不是地址相等)
组合断言
assertAll
方法接受多个 org.junit.jupiter.api.Executable
函数式接口的实例(空参空返回)作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
@Test
public void testAssertions() {
// 都成功才算成功
assertAll("test",
() -> assertTrue(true && true),
() -> assertEquals(5, sum(2, 3)));
}
异常断言
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。
而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。
// 断言业务要抛出异常(意思的断言这个业务要抛出异常,)
assertThrows(ArithmeticException.class, () -> {int i = 10 / 0;}, "业务路径竟然正常运行");
超时断言
Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
快速失败
通过 fail 方法直接使得测试失败
if (...) {
fail("测试失败");
}
JUnit 5 中的前置条件(assumptions,假设)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
失败了在测试报告上不显示fail,而是skiped(和@disabled一样)
import static org.junit.jupiter.api.Assumptions.*;
public class Junit5Test {
String environment = "DEV";
@Test
public void testAssumptions() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD")); // 不满足条件会使得测试执行终止
//只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
@Nested加在里层的测试类上面,表示嵌套测试。对于before/after只对于同层或者更里层的测试有效,对于外层不生效
@DisplayName("A stack")
public class TestAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
assertNull(stack); // 这时stack为空,外层的Test不能驱动内层的BeforeEach/All之类的方法提前或之后运行
}
@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);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() { // 内层的Test可以驱动外层的BeforeEach/All之类的方法提前或之后运行
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());
}
}
}
}
参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。⭐
利用 @ValueSource 等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
public class ParameterTest {
@ParameterizedTest // 当前测试方法是参数化测试
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
// 注意方法返回需要是一个流,而且是静态的
static Stream<String> method() {
return Stream.of("apple", "banana", "sutong");
}
}
在进行迁移的时候需要注意如下的变化:
注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
把@Before 和@After 替换成@BeforeEach 和@AfterEach。
把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
把@Ignore 替换成@Disabled。
把@Category 替换成@Tag。
把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
1.x和2.x版本区别(SpringBoot1.x版本就是Actuator1.x版本):
使用:
引入场景
访问http://localhost:8080/actuator/**
(有项目路径加上项目路径)
http://localhost:8080/actuator/health
当前健康状态 ,http://localhost:8080/actuator/info
当前应用的详细信息
health,info…称为Endpoint
,SpringBoot官方有非常多的EndPoint
暴露所有监控信息为HTTP(默认大多数都不以HTTP方式暴漏)
management:
endpoints:
enabled-by-default: true # 默认开启所有监控端点(默认也是true)
web:
exposure:
include: '*' # 以web方式暴露所有端点(默认只有health,info)
支持的暴露方式
HTTP:默认只暴露health和info Endpoint
JMX:默认暴露所有Endpoint (例如Jconsole)
除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
health,info…称为Endpoint
,SpringBoot官方有非常多的EndPoint
官方文档:https://docs.spring.io/spring-boot/docs/2.3.4.RELEASE/reference/html/production-ready-features.html#production-ready-endpoints
最常使用的端点:
ID | 描述 |
---|---|
auditevents |
暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件 。 |
beans |
显示应用程序中所有Spring Bean的完整列表。 |
caches |
暴露可用的缓存。 |
conditions |
显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
configprops |
显示所有@ConfigurationProperties 。 |
env |
暴露Spring的属性ConfigurableEnvironment |
flyway |
显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway 组件。 |
health |
显示应用程序运行状况信息。 |
httptrace |
显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository 组件。 |
info |
显示应用程序信息。 |
integrationgraph |
显示Spring integrationgraph 。需要依赖spring-integration-core 。 |
loggers |
显示和修改应用程序中日志的配置。 |
liquibase |
显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase 组件。 |
metrics |
显示当前应用程序的“指标”信息。 |
mappings |
显示所有@RequestMapping 路径列表。 |
scheduledtasks |
显示应用程序中的计划任务。 |
sessions |
允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 |
shutdown |
使应用程序正常关闭。默认禁用。 |
startup |
显示由ApplicationStartup 收集的启动步骤数据。需要使用SpringApplication 进行配置BufferingApplicationStartup 。 |
threaddump |
执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump |
返回hprof 堆转储文件。 |
jolokia |
通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core 。 |
logfile |
返回日志文件的内容(如果已设置logging.file.name 或logging.file.path 属性)。支持使用HTTPRange 标头来检索部分日志文件的内容。 |
prometheus |
以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus 。 |
最常用的Endpoint
Health:健康状况
Metrics:运行时指标
Loggers:日志记录
可视化平台
https://github.com/codecentric/spring-boot-admin
Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
management:
...
endpoint: # 对某个端点的具体配置
health:
show-details: always # 显示健康的详细信息
health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告(全健康才健康)
很多的健康检查默认已经自动配置好了,比如:数据库、redis等
可以很容易的添加自定义的健康检查机制
Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
通过Metrics对接多种监控系统
简化核心Metrics开发
添加自定义Metrics或者扩展已有Metrics
管理Endpoints
默认所有的Endpoint
除过shutdown
都是开启的。但有时候我们需要选择开启,即或者禁用所有的Endpoint
然后手动开启指定的Endpoint
management:
endpoints:
enabled-by-default: false # 关掉监控所有的端点(关闭总开关)!!下面选择开启端点
web:
exposure:
include: '*'
endpoint:
health:
enabled: true
info:
enabled: true
metrics:
enabled: true
定制 Health
信息
// 后缀必须是HealthIndicator,前缀就是端点的名字,继承AbstractHealthIndicator或者实现HealthIndicator接口
// 可以参考 磁盘监控 -> DiskSpaceHealthIndicator写法
@Component // 放到容器中就行了
public class MyComHealthIndicator extends AbstractHealthIndicator {
// 编写真实的检查方法
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// 详细信息
Map<String,Object> map = new HashMap<>();
// 检查完成
if(...) {
builder.up(); //健康 或者builder.status(Status.UP);
map.put("count", 1);
map.put("ms", 100);
} else {
builder.down(); // 不健康 或者builder.status(Status.DOWN);
map.put("err", "连接超时");
map.put("ms", 3000);
}
builder.withDetail("code", 100).withDetails(map);
}
}
定制 info
信息 (两种)
编写配置文件
info:
appName: sutong-learn # 自定义
appVersion: 1.0
mavenProjectName: @project.artifactId@ # 使用@@获取pom文件里面的值
mavenProjectVersion: @project.version@
实现InfoContributor
// 这个类后缀可以随便
@Component
public class ExampleInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example", "hello")
.withDetails(Collections.singletonMap("key", "value"));
}
}
定制Metrics
信息 (后面也经常需要定制的)
默认支持是很多…看文档
增加定制Metrics:
@Service
public class EmpService {
@Autowired
private EmpMapper empMapper;
private Counter counter;
// 构造器自动注入
public EmpService(MeterRegistry meterRegistry) {
counter = meterRegistry.counter("empService.getEMpById被调用的次数"); // 参数name是指标名
}
public Emp getEMpById(Integer id) {
counter.increment(); // 每调用一次增加一次
return empMapper.getEmpById(id);
}
}
//------------------------------也可以使用下面的方式--------------------------------------
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
定制自己的监控端点,EndPoint
@Component
@Endpoint(id = "myservice") // id是端点名
public class MyServiceEndPoint {
// 返回值无所谓,ReadOperation代表是个读方法,即返回端点的数据。因为是getXxx,必须是无参是方法
// http://localhost:8080/actuator/myservice
@ReadOperation
public Map<String, String> getDockerInfo() {
return Collections.singletonMap("info", "docker started...");
}
// 写操作
@WriteOperation
private void restartDocker() {
System.out.println("docker restarted....");
}
}
场景:开发ReadinessEndpoint来管理程序是否就绪,或者LivenessEndpoint来管理程序是否存活;
当然,这个也可以直接使用https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes
AdminServer文档:https://codecentric.github.io/spring-boot-admin/2.3.1/
准备一个SpringBootServer服务器,只需要加上web场景,再加上下面的starter
(这个就能监控我们的项目了,专门来收集其他微服务监控数据的)
<dependency>
<groupId>de.codecentricgroupId>
<artifactId>spring-boot-admin-starter-serverartifactId>
<version>2.3.1version>
dependency>
还要再在主程序上写上@EnableAdminServer
注解
因为我们要监控是项目是8080端口,所以这个项目要改一下端口例如8888 server.port=8888
访问http://localhost:8888/
就能看到监控页了(但现在还没有数据)
注册客户端
在我们要监控的项目中引入下面的starter:
<dependency>
<groupId>de.codecentricgroupId>
<artifactId>spring-boot-admin-starter-clientartifactId>
<version>2.3.1version>
dependency>
配置URL:
spring:
application:
name: springboot02 # 给当前应用取个名字
boot:
admin:
client:
url: http://localhost:8888 # 填写AdminServer的URL
instance:
prefer-ip: true # 使用ip注册进来(写不写都行)
management:
endpoints:
web:
exposure:
include: '*' # 以web方式暴露所有端点
如果引入一些安全框架还要所有请求数放行等…(一般是要引入的)
没有的话直接启动我们要监控的项目就行,访问监控页就能看到数据了。
为了方便多环境适配,springboot简化了profile功能。
application-profile功能
默认配置文件 application.yaml
,任何时候都会加载
指定环境配置文件 application-{env}.yaml
激活指定环境
默认配置文件激活,application.yaml
里面:
# 如果这里和环境配置文件属性同名,则以指定的环境属性为准
spring:
profiles:
active: env # 写'-'后面的就行了
命令行激活
java -jar xxx.jar --spring.profiles.active=env --person.userName=haha
这里还可以修改配置文件的任意值,命令行优先!
默认配置与环境配置同时生效,同名配置项,profile配置优先
@Profile
条件装配功能
@Configuration(proxyBeanMethods = false)
@Profile("production") // production环境才进行配置(方法上面也行,配置绑定类上也行)
public class ProductionConfiguration {
// ...
}
profile分组
spring:
profiles:
active: production
group:
production[0]: proddb
production[1]: prodmq
# 即把production环境分成了两个配置文件
# properties
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
Spring Boot 允许您外部化配置,以便您可以在不同的环境中使用相同的应用程序代码,您可以使用各种外部配置源,常用的的见下面
外部配置源
常用:Java属性文件、YAML文件、环境变量(本机配置的环变量)、命令行参数;
@SpringBootApplication
public class Springboot02Application {
public static void main(String[] args) {
ConfigurableApplicationContext run =
SpringApplication.run(Springboot02Application.class, args);
ConfigurableEnvironment environment = run.getEnvironment();
Map<String, Object> systemEnvironment = environment.getSystemEnvironment(); // 系统环境变量
Map<String, Object> systemProperties = environment.getSystemProperties(); // 系统的属性
}
}
配置文件查找位置
classpath 根路径
classpath 根路径下config目录
jar包当前目录
jar包当前目录的config目录
jar包当前目录/config子的直接子目录(只能一级目录,名字随意,这条Liunx系统好像才行)
(如果有同名的配置,后面路径下的配置会覆盖前面的,即且越往下优先级越高)
配置文件加载顺序
当前jar包内部的application.properties和application.yml
当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
引用的外部jar包的application.properties和application.yml
引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
starter启动原理
starter-pom引入 autoconfigurer 包 (starter -> autoconfigure -> spring-boot-starter)
starter
里面并没有什么代码,只说明当前场景引入哪些依赖 (当然要引入autoconfigure包)
autoconfigure
包里面做好所有的自动配置功能。(编写自动配置类XxxAutoConfiguration
-> xxxxProperties
)
使用 @Configuration,@Conditional, @EnableConfigurationProperties, @Bean…等注解
包中配置使用 META-INF/spring.factories 中 **EnableAutoConfiguration **的值,使得项目启动加载指定的自动配置类!!!
starter的自定义
sutong-hello-spring-boot-starter(启动器,只列出依赖并没什么代码,并引入下面的 xxx-autoconfigure包)
sutong-hello-spring-boot-starter-autoconfigure(编写自动配置类,不要忘记写spring.factories文件)
(业务中一些非常常用的场景就可以像这样抽取出来)
Spring原理、SpringMVC原理、自动配置原理、SpringBoot原理
SpringBoot启动过程:(面试可能问⭐)
创建 SpringApplication
运行 SpringApplication
StopWatch
监听整个应用程序启动停止的监听器,里面记录应用的启动时间…
创建引导上下文(Context环境)DefaultBootstrapContext c = createBootstrapContext()
里面会获取到所有之前配置的 **Bootstrappers **取挨个执行 intitialize()
来完成对引导启动器上下文环境设置
让当前应用进入headless 模式。java.awt.headless (简言之就是自力更生模式)
获取所有 RunListener(运行监听器),为了方便所有Listener进行事件感知
获取方式还是取去spring.factories文件中找org.springframework.boot.SpringApplicationRunListener
遍历所有运行监听器 SpringApplicationRunListener 调用 starting()
方法
(相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting)
保存命令行参数,ApplicationArguments
(即主方法里面的args参数)
准备环境 prepareEnvironment()
environmentPrepared()
通知所有的监听器当前环境准备完成创建IOC容器 createApplicationContext()
根据项目类型创建对应容器(当前是Servlet,即AnnotationConfigServletWebServerApplicationContext)
准备ApplicationContext,即IOC容器的基本信息, prepareContext()
保存环境信息
IOC容器的后置处理流程
应用初始化器,applyInitializers()
遍历所有的ApplicationContextInitializer调用其initialize()
,来对ioc容器进行初始化扩展功能
遍历所有的 运行监听器 SpringApplicationRunListener调用 contextPrepared()
,通知所有的监听器上下文准备好了
再次遍历监听器 调用其contextLoaded()
,通知所有的监听器 上下文加载好了
刷新IOC容器,refreshContext()
里面就是SpringIOC的关键代码,创建容器中的所有组件(去看Spring注解版视频有讲)
容器刷新完成后工作,afterRefresh()
遍历所有的 运行监听器 SpringApplicationRunListener调用 started()
,通知所有监控器启动好了
调用所有runners,callRunners()
run()
方法如果以上有异常,遍历所有的 运行监听器 SpringApplicationRunListener调用 failed()
再次遍历所有的 运行监听器 SpringApplicationRunListener调用 running()
, 通知所有的监听器 正在运行
running如果有问题,继续通知 failed
,调用所有 Listener 的 failed()
,通知所有的监听器 当前失败了
关键组件:ApplicationContextInitializer,ApplicationListener, SpringApplicationRunListener,ApplicationRunner ,CommandLineRunner
前三个自定义的话需要配置在spring.factories文件里面,后两个需要放到容器中
详解refreshContext()
-> AbstractApplicationContext.refresh()
:
prepareRefresh()
刷新前的预处理工作
initPropertySources()
初始化一些属性设置,进去是空的留给子类做事情的earlyApplicationEvents = new ListHashSet
保存容器的一些早期的事件obtainFreshBeanFactory()
获取BeanFactory
refreshBeanFactory()
-> beanFactory = new DefaultListableBeanFactory()
创建了一个BeanFactory对象,设置序列化idgetBeanFactory()
返回刚刚 GenericApplicationContext
创建的BeanFactory,给本类使用prepareBeanFactory(beanFactory)
预处理BeanFactory,做一些设置
设置类加载器,支持的的表达式解析器…
添加部分BeanPostProcessor后置处理器,例如ApplicationCotextAwareProcessor
设置忽略的自动装配的接口,例如EnvironmentAware,EmbeddedValueResolverAware…
注册可以解析的自动装配:我们能在任何组件中自动注入,例如BeanFactory,ResourceLoader,ApplicationEventPublisher,ApplicationContext
添加默认的AspectJ支持
给BeanFactoy中注册一些能用的组件,
例如:environment(ConfigurableEnvironment),systemProperties(Map),systemEnvironment(Map)要用也可也自动注入
postProcessBeanFactory(beanFactory)
BeanFactory准备工作完成后进行的后置处理工作
进去是空的,子类可以重写在BeanFactory创建并预准备完成做进一步的设置 (以上都是BeanFactory的创建以及准备工作!)
invokeBeanFactoryPostProcessors(beanFactory)
执行所有的BeanFactory后置处理器
这步不仅执行BeanFactoryPostProcessor(在BeanFactory标准初始化之后,而且Bean实例还未创建的时候执行,后), 执行时都有优先级排序先后,还要执行BeanDefinitionRegistryPostProcessor (在所有Bean定义信息将要被加载的时候执行,前)
registerBeanPostProcessors(beanFactory)
注册所有的Bean后置处理器,(作用:拦截Bean的创建过程)
BeanPostProcessor 子接口:DestructionAwareBeanPostProcessor,InstantiationAwareBeanPostProcessor,SmartInstantiationAwareBeanPostProcessor,MergedBeanDefinitionPostProcessor…不同类型的接口在Bean创建前后执行时机可能是不一样,这个执行的时候也会有优先级排序
initMessageSource()
初始化MessageSource组件(做国际化,消息解析,消息绑定功能…)
initApplicationEventMulticaster()
初始化事件派发器
onRefresh()
进去是空的,留给子类(子容器)重写,在容器刷新的时候可以自定义一些逻辑
registerListeners()
将项目中所有的监听器(ApplicationListener)注册进来
finishBeanFactoryInitialization(beanFactory)
初始化剩下的所有的单实例Bean
-> beanFactory.preInstantiateSingletons()
先拿到所有Bean的定义信息,依次进行创建对象和初始化(有些Bean可能在前面以及创建完了)
获取Bean的定义信息,判断,如果不是抽象的,是单实例的,不是懒加载的
再判断是否是FactoryBean,如果是则调用FactoryBean的getObject()
创建Bean。
不是则调用getBean(beanName)
创建对象 -> doGetBean(..)
:
先获取缓存中保存的单实例Bean,如果能获取到,说明这个Bean之前以及被创建过了
(所有的单实例Bean创建完都会保存到缓存中,就是个 Map类型,叫singletObjects)
缓存中获取不到则开始Bean的创建流程
先标记当前Bean已经被创建(防止多线程下重复创建),拿到Bean的定义信息
获取当前Bean依赖的其他Bean,如果有的话还是调用getBean()
把依赖的Bean先创建出来
启动单实例的创建流程:createBean(beanName, mbd, args)
:
resolveBeforeInstantiation(..)
让BeanPostProcessor (InstantiationAwareBeanPostProcessor
类型的后置处理器)拦截Bean的创建,调用applyBeanPostProcessorsBeforeInstantiation(),applyBeanPostProcessorsAfterInitialization()方法,如果了返回代理对象,则Bean创建成功,直接return。如果没有返回对象,则进入下一步
doCreateBean(..)
-> createBeanInstance(...)
创建Bean实例,里面利用工厂方法或者对象构造器创建实例
-> applyMergedBeanDefinitionPostProcessors(..)
执行MergedBeanDefinitionPostProcessor
类型的后置处理器postProcessMergedBeanDefinition()方法
-> populateBean(..)
为Bean属性赋值,赋值之前拿到InstantiationAwareBeanPostProcessor
类型的后置处理器,执行其postProcessAfterInstantiation()方法,再执行postProcessPropertyValues()方法,最后才进行赋值操作applyPropertyValues()
,应用Bean属性的值
-> initializeBean(..)
Bean初始化,首先执行XxxAware接口的方法(回调)。然后执行后置处理器初始化之前的方法,即执行BeanPostProcessor
类型的后置处理器的postProcessBeforeInitialization()方法。然后执行Bean的指定的初始化方法。然后执行后置处理器初始化之后的方法,即调用postProcessBeforeInitialization()方法。
-> registerDisposableBeanIfNecessary(..)
注册Bean的销毁方法
创建完成,则把单实例的Bean放到缓存中,singletObjects中,IOC容器就是这些Map,保存了单实例Bean,环境信息…
遍历完所有的Bean信息,并利用getBean()创建成功后,来检查所有的Bean是否实现SmartInitializingSingleton
接口
如果是则执行其afterSingletonsInstantiated()方法
finishRefresh()
上面完成BeanFactory的初始化创建过程,IOC创建完成
-> initLifecycleProcessor()
初始化和生命周期有关的后置处理器,先判断是否有id为lifecycleProcessor的组件,有则使用,没有则创建一个默认的DefaultLifecycleProcessor,并注册到容器中(可以写LifecycleProcessor实现类,可以在BeanFactory刷新完成以及关闭的时候进行调用)
-> getLifecycleProcessor().onRefresh()
拿到到前面定义的声明周期处理器(监听BeanFactory的) 回调onReFresh()
-> publishEvent(new ContextRefreshedEvent(this))
发布容器刷新完成事件
简单总结:
- Spring容器再启动的时候,先会保存所有注册进来的Bean的定义信息
- Spring容器会再合适的时机创建这些Bean(时机1:用到这个Bean的时候。时机2:统一创建剩下所有Bean的时候)
- 后置处理器:每一个Bean创建完成,都会使用各种后置处理器进行处理,增强Bean的功能(自动注入,AOP功能…)
是空的,子类可以重写在BeanFactory创建并预准备完成做进一步的设置 (以上都是BeanFactory的创建以及准备工作!)
invokeBeanFactoryPostProcessors(beanFactory)
执行所有的BeanFactory后置处理器
这步不仅执行BeanFactoryPostProcessor(在BeanFactory标准初始化之后,而且Bean实例还未创建的时候执行,后), 执行时都有优先级排序先后,还要执行BeanDefinitionRegistryPostProcessor (在所有Bean定义信息将要被加载的时候执行,前)
registerBeanPostProcessors(beanFactory)
注册所有的Bean后置处理器,(作用:拦截Bean的创建过程)
BeanPostProcessor 子接口:DestructionAwareBeanPostProcessor,InstantiationAwareBeanPostProcessor,SmartInstantiationAwareBeanPostProcessor,MergedBeanDefinitionPostProcessor…不同类型的接口在Bean创建前后执行时机可能是不一样,这个执行的时候也会有优先级排序
initMessageSource()
初始化MessageSource组件(做国际化,消息解析,消息绑定功能…)
initApplicationEventMulticaster()
初始化事件派发器
onRefresh()
进去是空的,留给子类(子容器)重写,在容器刷新的时候可以自定义一些逻辑
registerListeners()
将项目中所有的监听器(ApplicationListener)注册进来
finishBeanFactoryInitialization(beanFactory)
初始化剩下的所有的单实例Bean
-> beanFactory.preInstantiateSingletons()
先拿到所有Bean的定义信息,依次进行创建对象和初始化(有些Bean可能在前面以及创建完了)
获取Bean的定义信息,判断,如果不是抽象的,是单实例的,不是懒加载的
再判断是否是FactoryBean,如果是则调用FactoryBean的getObject()
创建Bean。
不是则调用getBean(beanName)
创建对象 -> doGetBean(..)
:
先获取缓存中保存的单实例Bean,如果能获取到,说明这个Bean之前以及被创建过了
(所有的单实例Bean创建完都会保存到缓存中,就是个 Map类型,叫singletObjects)
缓存中获取不到则开始Bean的创建流程
先标记当前Bean已经被创建(防止多线程下重复创建),拿到Bean的定义信息
获取当前Bean依赖的其他Bean,如果有的话还是调用getBean()
把依赖的Bean先创建出来
启动单实例的创建流程:createBean(beanName, mbd, args)
:
resolveBeforeInstantiation(..)
让BeanPostProcessor (InstantiationAwareBeanPostProcessor
类型的后置处理器)拦截Bean的创建,调用applyBeanPostProcessorsBeforeInstantiation(),applyBeanPostProcessorsAfterInitialization()方法,如果了返回代理对象,则Bean创建成功,直接return。如果没有返回对象,则进入下一步
doCreateBean(..)
-> createBeanInstance(...)
创建Bean实例,里面利用工厂方法或者对象构造器创建实例
-> applyMergedBeanDefinitionPostProcessors(..)
执行MergedBeanDefinitionPostProcessor
类型的后置处理器postProcessMergedBeanDefinition()方法
-> populateBean(..)
为Bean属性赋值,赋值之前拿到InstantiationAwareBeanPostProcessor
类型的后置处理器,执行其postProcessAfterInstantiation()方法,再执行postProcessPropertyValues()方法,最后才进行赋值操作applyPropertyValues()
,应用Bean属性的值
-> initializeBean(..)
Bean初始化,首先执行XxxAware接口的方法(回调)。然后执行后置处理器初始化之前的方法,即执行BeanPostProcessor
类型的后置处理器的postProcessBeforeInitialization()方法。然后执行Bean的指定的初始化方法。然后执行后置处理器初始化之后的方法,即调用postProcessBeforeInitialization()方法。
-> registerDisposableBeanIfNecessary(..)
注册Bean的销毁方法
创建完成,则把单实例的Bean放到缓存中,singletObjects中,IOC容器就是这些Map,保存了单实例Bean,环境信息…
遍历完所有的Bean信息,并利用getBean()创建成功后,来检查所有的Bean是否实现SmartInitializingSingleton
接口
如果是则执行其afterSingletonsInstantiated()方法
finishRefresh()
上面完成BeanFactory的初始化创建过程,IOC创建完成
-> initLifecycleProcessor()
初始化和生命周期有关的后置处理器,先判断是否有id为lifecycleProcessor的组件,有则使用,没有则创建一个默认的DefaultLifecycleProcessor,并注册到容器中(可以写LifecycleProcessor实现类,可以在BeanFactory刷新完成以及关闭的时候进行调用)
-> getLifecycleProcessor().onRefresh()
拿到到前面定义的声明周期处理器(监听BeanFactory的) 回调onReFresh()
-> publishEvent(new ContextRefreshedEvent(this))
发布容器刷新完成事件
简单总结:
- Spring容器再启动的时候,先会保存所有注册进来的Bean的定义信息
- Spring容器会再合适的时机创建这些Bean(时机1:用到这个Bean的时候。时机2:统一创建剩下所有Bean的时候)
- 后置处理器:每一个Bean创建完成,都会使用各种后置处理器进行处理,增强Bean的功能(自动注入,AOP功能…)
- 事件驱动模式:ApplicationListener,做事件监听。ApplicationEventMulticaster:事件派发
本笔记参考视频:SpringBoot2