SpringBoot2知识学习及查漏补缺
基本介绍
优缺点
优点:
-
创建独立Spring应用
-
内嵌web服务器
-
自动starter依赖,简化构建配置
-
自动配置Spring以及第三方功能
-
提供生产级别的监控、健康检查及外部化配置
-
无代码生成、无需编写XML
缺点:
- 人称版本帝,迭代快,需要时刻关注变化
- 封装太深,内部原理复杂,不容易精通
时代背景
微服务
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
分布式
分布式的困难:
- 远程调用
- 服务发现
- 负载均衡
- 服务容错
- 配置管理
- 服务监控
- 链路追踪
- 日志管理
- 任务调度
分布式的解决方案:
- SpringBoot + SpringCloud
云原生
上云的困难:
- 服务自愈
- 弹性伸缩
- 服务隔离
- 自动化部署
- 灰度发布
- 流量治理
上云的解决方案:
- docker容器化技术
- k8s容器编排
- DevOps,CI/CD
- Service Mesh于Serviceless
相关文档
查看版本新特性:
Home · spring-projects/spring-boot Wiki (github.com)
官方文档:
Spring Boot Reference Documentation
自动配置原理
注解说明
@Configuration
-
加在类上,告诉SpringBoot这是一个配置类
-
配置类里面使用
@Bean
标注在方法上给容器注册组件,默认也是单实例的 -
配置类本身也是组件
-
proxyBeanMethods属性:标识配置类是否是代理@Bean方法
- Full模式(proxyBeanMethods = true):保证每个@Bean方法被调用多少次返回的组件都是单实例的
- Lite模式(proxyBeanMethods = false):每个@Bean方法被调用多少次返回的组件都是新创建的
- 组件依赖必须使用Full模式,其他情况可以使用Lite模式(加快项目启动)
1 |
|
1 |
|
@Bean、@Component、@Controller、@Service、@Repository
标识类是一个组件,被spring管理
@ComponentScan
组件扫描
@Import
在已标记为组件的类上使用
自动给导入的类型注册为组件,默认组件的名称为全类名
1 |
|
1 |
|
@Conditional
按照条件装配,当满足指定的条件时才将组件注入
- @ConditionalOnBean:当容器中存在某些组件
- @ConditionalOnMissingBean:当容器中不存在某些组件
- @ConditionalOnClass:当容器中有某些类(有依赖jar包)
- @ConditionalOnMissingClass:当容器中没有某些类
- @ConditionalOnResource:当项目路径下存在某些资源
- @ConditionalOnJava:当是指定的Java版本号
- @ConditionalOnWebApplication:当工程是web应用
- @ConditionalOnNotWebApplication:当工程不是web应用
- @ConditionalOnProperty:当配置文件中配置了某些属性
@ImportResource
导入spring的xml配置文件
1 |
|
配置绑定
@ConfigurationProperties
添加在类上,prefix属性指定在配置文件中的前缀
1 |
|
1 |
|
只有注册到容器中才能自动加载
注册方式有两种:
-
类上加上
@Component
注解1
2
3@Component
@ConfigurationProperties(prefix = "demo.config")
public class DemoProperties { -
在其他组件上加上
@EnableConfigurationProperties
注解, 并指定注册的类1
2
3@Configuration
@EnableConfigurationProperties(DemoProperties.class)
public class DemoConfig {}
引导加载自动配置类
程序启动类上加的@SpringBootApplication
注解,是由三个注解构成
1 |
|
@SpringBootConfiguration
1 |
|
由以上代码可以看出,加上@SpringBootConfiguration注解后会把类标记为配置类
@ComponentScan
组件扫描
@EnableAutoConfiguration
开启自动配置
1 |
|
-
@AutoConfigurationPackage:
1
2
3// 给容器中导入一个组件,通过Registrar将启动类所在包下的所有组件注册到容器
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {} -
@Import(AutoConfigurationImportSelector.class)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 获取所有需要导入的组件
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}-
利用
getAutoConfigurationEntry
给容器中批量导入一些组件 -
调用
getCandidateConfigurations
获取到所有需要导入到容器中的配置类 -
利用工厂加载
loadSpringFactories
得到所有的组件 -
从
META-INF/spring.factories
位置来加载一个文件- 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
- spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories
- springboot2.7以上不推荐将自动配置类写到 META-INF/spring.factories,而是推荐写到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
- 总之就是在这些文件中已经写死了springboot一启动就要给容器中加载的所有配置类
-
按需开启自动配置项
按照上面一步操作后所有自动配置类启动时默认全部加载,但是按照条件装配规则(@Conditional),最终会按需配置
总结
-
SpringBoot先加载所有的自动配置类 xxxAutoConfiguration
-
每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxProperties里面拿。xxxProperties和配置文件进行了绑定
-
生效的配置类就会给容器中装配很多组件
-
只要容器中有这些组件,相当于这些功能就有了
-
定制化配置
-
- 用户直接自己@Bean替换底层的组件
- 用户去看这个组件是获取的配置文件什么值就去修改
最佳实践
-
引入场景依赖
-
查看自动配置了哪些(可选)
-
- 自己分析
- 配置文件中
debug=true
开启自动配置报告,启动后会输出生效于未生效配置的日志,Negative(不生效)\Positive(生效)
-
是否需要修改
-
- 参照文档修改配置项
-
- 自定义加入或者替换组件
-
-
- @Bean、@Component
-
-
- 自定义器 XXXXXCustomizer
YAML
基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- '#'表示注释
- 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义
自定义配置类提示
pom中引入依赖
1 |
|
增加构建插件
1 |
|
Web开发
SpringMVC自动配置
Spring Boot为Spring MVC提供了自动配置功能,对大多数应用程序都很适用。
自动配置在Spring的默认值基础上增加了以下功能。
- 包含了
ContentNegotiatingViewResolver
和BeanNameViewResolver
Bean - 支持为静态资源提供服务,包括对WebJars的支持
- 自动注册
Converter
、GenericConverter
和Formatter
Bean - 支持
HttpMessageConverters
- 自动注册
MessageCodesResolver
- 支持静态的
index.html
- 自动使用
ConfigurableWebBindingInitializer
bean
如果你想保留那些Spring Boot MVC定制,并进行更多的 MVC定制(Interceptor、Formatter、视图控制器和其他功能),你可以添加你自己的 @Configuration
类,类型为 WebMvcConfigurer
,但 不 含 @EnableWebMvc
。
如果你想提供 RequestMappingHandlerMapping
、RequestMappingHandlerAdapter
或 ExceptionHandlerExceptionResolver
的自定义实例,并仍然保持Spring Boot MVC的自定义,你可以声明一个 WebMvcRegistrations
类型的bean,用它来提供这些组件的自定义实例。
如果你想完全控制Spring MVC,你可以添加你自己的 @Configuration
并使用 @EnableWebMvc
注解 ,或者添加你自己的 @Configuration
并使用 DelegatingWebMvcConfiguration
注解 ,如 @EnableWebMvc
的Javadoc中所述。
HttpMessageConverter
Spring MVC使用 HttpMessageConverter
接口来转换HTTP请求和响应
如果需要添加或定制转换器,你可以使用Spring Boot的 HttpMessageConverters
类
1 |
|
静态内容
默认情况下,Spring Boot从classpath中的 /static
(或 /public
或 /resources
或 /META-INF/resources
)目录或 ServletContext
的root中提供静态内容。
资源访问路径:项目访问根路径/静态资源名
它使用了Spring MVC中的 ResourceHttpRequestHandler
,因此你可以通过添加你自己的 WebMvcConfigurer
和覆盖 addResourceHandlers
方法来修改该行为。
改变默认的静态资源路径
即访问路径前缀,默认是/**
1 |
|
做以上配置后的资源访问路径为:项目访问根路径/resources/静态资源名
自定义静态资源文件存放位置
1 |
|
配置了自定义的存放位置后,默认的4个路径就会失效
欢迎页面
Spring Boot同时支持静态和模板化的欢迎页面。 它首先在配置的静态内容位置寻找一个 index.html
文件。 如果没有找到,它就会寻找 index
模板。 如果找到了其中之一,它就会自动作为应用程序的欢迎页面使用。
访问 项目访问根路径/或项目访问根路径/index.html 会解析到欢迎页面
若配置了自定义的静态资源路径则失效
静态内容加载原理
-
Springboot启动后加载各依赖组件的自动配置类
-
SpringMVC功能的自动配置类为
WebMvcAutoConfiguration
1
2
3
4
5
6
7@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebMvcAutoConfiguration {} -
内部有一个
WebMvcAutoConfigurationAdapter
配置类1
2
3
4
5@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {} -
将配置文件与
WebMvcProperties
和WebProperties
类进行绑定 -
执行
addResourceHandlers
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// add-mappings=false时禁用静态资源访问
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
// 将访问 /webjars/** 的请求加入到资源处理器
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
// 将访问 static-path-pattern 配置的请求加入到资源处理器,并指定资源路径为 static-locations 配置的值
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);
}
});
}
请求参数处理
HiddenHttpMethodFilter
通过表单隐藏域,以post方式提交一个_method
字段给服务器,通过HiddenHttpMethodFilter处理包装后,请求的method变为了传入的合法方式,以此实现RESTful风格的接口
WebMvcAutoConfiguration类中有以下代码
1 |
|
通过上面代码可以知道,要开启还需要进行以下配置
1 |
|
请求映射原理
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet->doDispatch()开始
doDispatch调用getHandler方法获取处理器
1 |
|
RequestMappingHandlerMapping
保存了所有@RequestMapping和handler的映射规则
所有的请求映射都在HandlerMapping中
- SpringBoot自动配置了 WelcomePageHandlerMapping、RequestMappingHandlerMapping
- 请求进来后会遍历所有的HandlerMapping,来查找映射的handler
- 当需要自定义映射处理时,可以自己自定义HandlerMapping,并注册到容器中
参数注解
常见的有@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
@MatrixVariable
@MatrixVariable即矩阵变量
1 |
|
参数处理原理
-
doDispatch方法中先充HandlerMapping中找到处理请求的handler(Controller.methd())
-
找到当前handler的适配器HandlerAdapter(RequestMappingHandlerAdapter)
1
2
3
4
5
6
7
8
9
10
11protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}- RequestMappingHandlerAdapter:支持方法上标注@RequestMapping
- HandlerFunctionAdapter:支持函数式编程的
-
执行适配器的handle方法,调用
RequestMappingHandlerAdapter
的handleInternal
方法1
2
3
4
5
6
7
8
9
10@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
// ...
mav = invokeHandlerMethod(request, response, handlerMethod);
// ...
return mav;
} -
执行invokeHandlerMethod方法,方法内会设置
参数解析器
1
2
3if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
由上面的参数解析器可以知道,SpringMVC目标方法能写多少种参数类型,取决于参数解析器
-
调用invokeAndHandle->invokeForRequest->getMethodArgumentValues方法,解析参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35protected 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[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;
} -
resolveArgument
方法中会创建WebDataBinder
对象,该对象中的GenericConversionService
属性包含大量类型转换器,用于将字符串转换为对应的类型 -
将所有的数据都放在
ModelAndViewContainer
,包含要去的页面地址,以及Model(BingdingAwareModelMap)数据 -
执行
processDispatchResult
->render->renderMergedOutputModel
自定义类型转换器
前端提交数据格式
后端接收格式
1 |
|
类型转换器
1 |
|
数据响应与内容协商
数据响应流程
1 |
|
请求上面接口后,返回值处理过程如下
-
执行invokeForRequest方法后获取到返回值,遍历返回值解析器对返回值进行处理
1
2
3
4try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}- SpringMVC支持以下类型的返回值
- ModelAndView
- Model
- View
- ResponseBodyEmitter
- StreamingResponseBody
- HttpEntity
- HttpHeaders
- Callable
- DeferredResult
- AsyncTask
- 方法有@ModelAttribute注解
- @ResponseBody注解
- void或字符串
- Map
- 在默认解析模式下,对于任何不是简单类型的返回值类型
- SpringMVC支持以下类型的返回值
-
遍历找到支持返回值类型的解析器后,执行解析器的
handleReturnValue
方法 -
RequestResponseBodyMethodProcessor处理器可以处理加了@ResponseBody注解的
-
执行
writeWithMessageConverters
方式,会利用消息转换器(MessageConverters)来处理 -
若返回数据不是流类型(InputStreamResource、Resource)的,会进行
内容协商
(浏览器在请求头中告诉服务器它可以接受什么类型的内容) -
内容协商完成获取到
mediaType
-
遍历所有消息转换器,判断是否支持(supports)返回值类型,是否可以写为(canWrite)mediaType类型的内容
- byte:[application/octet-stream, */*]
- String:[text/plain, */*]
- String:[text/plain, */*]
- Resource:[*/*]
- ResourceRegion:[application/xml, text/xml, application/*+xml]
- DOMSource、SAXSource、StAXSource、StreamSource、Source
- MultiValueMap:[application/x-www-form-urlencoded, multipart/form-data, multipart/mixed]
- 任意类型:[application/json, application/*+json]
- 任意类型:[application/json, application/*+json]
- 支持注解方式xml处理
-
最终 MappingJackson2HttpMessageConverter 把对象转为JSON写到响应体中
内容协商原理
客户端通过请求头中不同的Accept
值来告诉服务器本客户端可以接收的数据类型
内容协商的代码在AbstractMessageConverterMethodProcessor
的writeWithMessageConverters
方法中
-
先从响应头中获取媒体类型,若没有则继续协商
1
2MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType(); -
从请求头中获取客户端支持的类型
1
acceptableTypes = getAcceptableMediaTypes(request);
-
遍历内容协商策略,默认使用基于请求头的策略,返回请求头中描述的支持的类型
1
2
3
4
5
6
7
8
9
10
11@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
} -
获取服务端能生成的媒体类型
1
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
-
获取服务端能生成且客户端支持的媒体类型
1
2
3
4
5
6
7
8List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
} -
按媒体类型权重值进行排序,并以第一个媒体类型作为最终使用的类型
1
2
3
4
5
6
7
8
9
10
11
12MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
} -
遍历消息解析器,获取支持生成对应媒体类型的解析器
1
2
3for (HttpMessageConverter<?> converter : this.messageConverters) {
// ...
} -
调用消息解析器的write方法将数据写到响应中
开启基于请求参数方式内容协商功能
修改配置文件:
1 |
|
请求格式:
1 |
|
自定义消息转换器
实现接口HttpMessageConverter
1 |
|
注册自定义的消息转换器
1 |
|
添加自定义类型到基于请求参数方式内容协商策略中
1 |
|
添加自定义的功能有可能会覆盖默认的很多功能,导致一些默认功能失效,需要谨慎使用
视图解析原理
- 入口还是DispatcherServlet的
doDispatch
方法 - HandlerAdapter通过反射执行完控制器方法后都会返回
ModelAndView
对象,该对象包含数据和视图地址 - 执行
processDispatchResult
方法,决定页面如何响应 - 调用render(mv, request, response)方法进行页面渲染逻辑
- 通过resolveViewName方法获取到View对象
- ContentNegotiationViewResolver中包含了所有支持的视图解析器,通过遍历视图解析器获取支持的解析器,然后解析得到视图对象
- 调用view.render(mv.getModelInternal(), request, response)进行页面渲染工作
拦截器
实现HandlerInterceptor
接口
1 |
|
注册拦截器
1 |
|
拦截器原理
-
doDispatch方法中调用
getHandler
获取到HandlerExecutionChain
对象,该对象包含了处理器和一个拦截器列表 -
按顺序执行所有拦截器的preHandle方法
-
若所有拦截器都放行,则执行目标方法
-
多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关
-
preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行
1
2
3
4
5
6com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor preHandle
com.zhuweitung.springboot2.interceptor.AdminHandlerInterceptor preHandle
com.zhuweitung.springboot2.interceptor.AdminHandlerInterceptor postHandle
com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor postHandle
com.zhuweitung.springboot2.interceptor.AdminHandlerInterceptor afterCompletion
com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor afterCompletion
-
-
若某个拦截器执行preHandle没有放行
-
preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行
1
2
3com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor preHandle
com.zhuweitung.springboot2.interceptor.AdminHandlerInterceptor preHandle
com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor afterCompletion
-
文件上传
1 |
|
配置文件上传大小
1 |
|
自动配置原理
-
自动配置类为
MultipartAutoConfiguration
,属性类为MultipartProperties
-
MultipartAutoConfiguration中将
StandardServletMultipartResolver
注册为multipartResolver(文件上传解析器) -
文件上传请求会进入DispatcherServlet的
doDispatch
方法 -
进入
checkMultipart
方法,使用配置的文件上传解析器判断参数是否是multipart(判断 ContentType 上是否以 multipart 开始)1
2
3
4
5@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(),
(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
} -
使用参数解析器
RequestPartMethodArgumentResolver
对参数进行解析 -
将request中文件信息封装为一个Map,MultiValueMap<String, MultipartFile>
-
解析完成后将MultipartFile数组交给控制器方法处理
异常处理
默认规则
- 默认情况下,Spring Boot提供
/error
处理所有错误的映射 - 对于浏览器,响应
whitelabel
错误视图,以html形式呈现错误信息 - 对于非浏览器客户端,响应json数据其中包含错误,HTTP状态和异常消息的详细信息
自动配置原理
-
ErrorMvcAutoConfiguration
是异常处理规则的自动配置类 -
注册了
DefaultErrorAttributes
(errorAttributes)组件,定义错误信息中包含哪些内容1
2@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {} -
注册了
BasicErrorController
组件,默认路径为/error,有两个处理方法,分别返回页面(new ModelAndView(“error”, model), error为下面注册的错误视图)和Json- 根据请求头中
Accept
是否为text/html
,进入不同方法
1
2
3@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {} - 根据请求头中
-
注册了一个id为
error
的错误视图 -
注册了
BeanNameViewResolver
视图解析器,作用是根据bean名称解析,服务于上面的错误视图 -
注册了
DefaultErrorViewResolver
,将不同错误渲染到不同页面上1
2
3
4
5
6
7
8
9
10
11
12
13
14
15static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
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;
}
异常处理流程
-
执行目标方法,目标方法有任何异常都会被catch,并被封装为dispatchException
-
进入
processDispatchResult
方法,进行视图解析 -
执行
processHandlerException
方法,处理异常1
2
3
4
5
6
7
8
9if (this.handlerExceptionResolvers != null) {
// 遍历所有处理器异常解析器来处理异常
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}- DefaultErrorAttributes将异常信息保存到request域,返回null视图
- 当没有解析器能够处理异常时,会继续向上抛出异常,底层会转达到/error
-
/error请求被BasicErrorController处理
- 解析错误视图,遍历所有ErrorViewResolver进行解析
- DefaultErrorViewResolver会根据响应状态码获取返回页面名称(先根据精确的状态码查找页面,若没有再通过模糊的,如4xx、5xx进行查找),并拼接为地址
- 模板引擎最终响应这个页面
自定义异常处理方式
-
自定义错误页
- error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
-
@ControllerAdvice
+@ExceptionHandler
处理全局异常;ExceptionHandlerExceptionResolver
来处理1
2
3
4
5
6
7
8@ControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler({CustomException.class})
@ResponseBody
public Result handleCustom(Exception exception) {
return Result.fail(exception.getMessage());
}
} -
@ResponseStatus
+自定义异常 ;ResponseStatusExceptionResolver
来处理 ,获取@ResponseStatus注解的信息,然后底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error1
2
3
4@ResponseStatus(value = HttpStatus.BAD_GATEWAY, reason = "访问太频繁")
public class TooManyRequestException extends RuntimeException {
private static final long serialVersionUID = 7699893349483938519L;
} -
Spring底层的异常,如参数类型转换异常;
DefaultHandlerExceptionResolver
处理框架底层的异常 -
自定义实现
HandlerExceptionResolver
接口来处理异常;可以作为默认的全局异常处理规则(优先级需要调高) -
实现接口
ErrorViewResolver
来自定义错误视图处理- 如ErrorMvcAutoConfiguration中配置了DefaultErrorViewResolver,通过错误码拼接为地址,渲染对应模板文件在呈现给客户端
Web原生组件注入
JavaWeb原生组件就是Servlet、Filter、Listener
使用注解
当使用嵌入式容器时,可以通过使用 @ServletComponentScan
来启用对 @WebServlet
、@WebFilter
和 @WebListener
注解的类的自动注册
这种方式注册的servlet不会经过spring的拦截器
使用RegistrationBean
可以使用 ServletRegistrationBean
、 FilterRegistrationBean
和 ServletListenerRegistrationBean
类
1 |
|
DispatchServlet注册的原理
DispatcherServletAutoConfiguration
类中配置了名称为dispatcherServlet
的DispatcherServlet
的BeanDispatcherServletRegistrationConfiguration
中通过RegistrationBean的方式注册了dispatcherServlet
数据访问
SQL
JDBC
添加JDBC场景依赖
1 |
|
这个场景包引入了数据库连接池、jdbc、事务等包,但是数据库驱动需要根据项目情况手动引入
添加数据库对应版本的驱动包
1 |
|
相关自动配置类:
- DataSourceAutoConfiguration:数据源的自动配置
- DataSourceTransactionManagerAutoConfiguration:事务管理器的自动配置
- JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置
- JndiDataSourceAutoConfiguration: jndi的自动配置
- XADataSourceAutoConfiguration:分布式事务相关的
添加连接配置和连接池配置
1 |
|
更换连接池
方式一:
添加连接池依赖
1 |
|
添加配置类来覆盖默认数据源配置
1 |
|
方式二:
添加连接池场景包
1 |
|
这个场景包除了引入了方式一的依赖,还引入了自动配置包
自动配置类为DruidDataSourceAutoConfigure
,导入了一下4个配置
- DruidSpringAopConfiguration:用于监控SpringBean
- DruidStatViewServletConfiguration:配置StatViewServlet(监控页面)
- DruidWebStatFilterConfiguration:配置WebStatFilter
- DruidFilterConfiguration:所有Druid自己filter的配置
添加连接配置和连接池配置
1 |
|
mybatis
-
添加场景包
1
2
3
4
5<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency> -
修改相关配置
1
2
3
4mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名映射 -
编写mapper接口,接口加上
@Mapper
注解,mybatis会自动扫描加上此注解的类- 使用
@MapperScan(basePackages="xxx")
注解,指定mapper接口包路径,就不需要在每个mapper接口上添加@Mapper注解了
- 使用
-
将mapper.xml文件放置对应目录下,与配置文件中
mapper-locations
描述的路径保持一致
mybatis-plus
-
添加场景包
1
2
3
4
5<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency> -
修改相关配置
1
2
3
4
5mybatis-plus:
# mapper-locations: classpath:mapper/*.xml # 有默认配置可以不配置
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名映射
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler # 枚举类处理 -
分页插件
1
2
3
4
5
6
7
8
9
10
11
12
13@Configuration
public class MybatisPlusConfig {
/**
* 开启分页插件
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
Redis
引入与配置
-
添加场景包
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
默认引入lettuce作为redis客户端
-
添加配置
1
2
3
4
5
6
7
8
9
10spring:
redis:
host: 192.168.0.123
port: 46379
password: 123456
database: 1
lettuce:
pool:
min-idle: 5
max-active: 50
自动配置
- RedisAutoConfiguration:redis的自动配置类
- 会导入LettuceConnectionConfiguration和JedisConnectionConfiguration,分别表示lettuce和jedis客户端的配置
- 注册了一个
RedisTemplate<Object, Object>
,键值都是object类型 - 注册了一个
StringRedisTemplate
,键值都是字符串类型
- RedisProperties:配置属性类
使用jedis
-
引入依赖
1
2
3
4<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency> -
修改配置文件
1
2
3spring:
redis:
client-type: jedis
使用
配置键值的序列化与反序列化器:
1 |
|
测试:
1 |
|
单元测试
JUnit5 的变化
Spring Boot 2.2.0 版本开始引入 JUnit5
作为单元测试默认库
JUnit5由三个不同的子项目组成,JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
- JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
- JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。
- JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。
注意:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)
引入
1 |
|
使用方式示例
1 |
|
常用注解
- **@Test :**表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
- **@ParameterizedTest :**表示方法是参数化测试
- **@RepeatedTest :**表示方法可重复执行
- **@DisplayName :**为测试类或者测试方法设置展示名称
- **@BeforeEach :**表示在每个单元测试之前执行
- **@AfterEach :**表示在每个单元测试之后执行
- **@BeforeAll :**表示在所有单元测试之前执行
- **@AfterAll :**表示在所有单元测试之后执行
- **@Tag :**表示单元测试类别,类似于JUnit4中的@Categories
- **@Disabled :**表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
- **@Timeout :**表示测试方法运行如果超过了指定时间将会返回错误
- **@ExtendWith :**为测试类或测试方法提供扩展类引用
断言
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证
这些断言方法都是 org.junit.jupiter.api.Assertions
的静态方法
简单断言
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
数组断言
通过 assertArrayEquals
方法来判断两个对象或原始类型的数组是否相等
1 |
|
组合断言
assertAll
方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
1 |
|
异常断言
用于测试方法抛出异常
1 |
|
超时断言
用于测试方法超时时间
1 |
|
快速失败
通过 fail 方法直接使得测试失败
1 |
|
前置条件
JUnit 5 中的前置条件(assumptions)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止
。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
1 |
|
assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。
assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
1 |
|
参数化测试
利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码
实现ArgumentsProvider
接口,任何外部文件都可以作为它的入参
常用注解
- @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
- @NullSource: 表示为参数化测试提供一个null的入参
- @EnumSource: 表示为参数化测试提供一个枚举入参
- @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
- @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
示例
1 |
|
指标监控
简介
SpringBoot Actuator,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
使用
-
引入依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency> -
修改配置
1
2
3
4
5
6
7
8
9management:
endpoints:
enabled-by-default: true # 暴露所有端点信息
web:
exposure:
include: '*' # 以web方式暴露
endpoint:
health: # 单独配置健康端点
show-details: always # 显示详细详细
常用端点
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应用程序,则可以使用以下附加端点:
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:日志记录
Health Endpoint
Health Endpoint返回当前服务的健康状态,端点开启show-details: always
后返回应用包含的一系列组件的健康状态
Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或者扩展已有Metrics
通过访问/actuator/metrics
查看所有的指标名称
再通过访问/actuator/metrics/{requiredMetricName}
查看指标的具体信息
自定义端点信息
自定义健康信息
需要实现HealthIndicator
接口或者继承AbstractHealthIndicator
抽象类
1 |
|
查看健康状态
自定义info信息
方式一:
在配置文件中配置
1 |
|
方式二:
实现InfoContributor
接口
1 |
|
自定义指标信息
比如说我想要获取文件上传次数的指标,可以使用以下方式实现
1 |
|
在其他场景中也可以使用以下方式
1 |
|
自定义端点
1 |
|
可视化
使用spring-boot-admin实现可视化,参考文档
监控可视化平台服务端
另起一个项目作为监控可视化平台
引入依赖
1 |
|
配置服务端口
1 |
|
客户端
客户端就是被监控的项目
引入依赖
1 |
|
修改配置
1 |
|
原理解析
Profile功能
用于多环境适配
application-profile功能
-
默认配置文件 application.yaml;任何时候都会加载
-
指定环境配置文件 application-{env}.yaml
- application-dev.yml
- application-prod.yml
-
激活指定环境
-
-
配置文件激活,在默认配置文件中进行以下配置
1
2
3spring:
profiles:
active: dev -
命令行激活:java -jar xxx.jar –spring.profiles.active=dev
-
-
默认配置与环境配置同时生效
-
同名配置项,profile配置优先
@Profile条件装配功能
标记类在指定环境下才生效
1 |
|
profile分组
1 |
|
spring.profiles.active指定组名来激活环境组
外部化配置
外部配置源
Java属性文件、YAML文件、环境变量、命令行参数
配置文件查找位置
以下位置的配置文件中,若出现相同配置项,从下到上依次覆盖
- classpath 根路径
- classpath 根路径下config目录
- jar包当前目录
- jar包当前目录的config目录
- /config子目录的直接子目录
配置文件加载顺序
- 当前jar包内部的application.properties和application.yml
- 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
- 引用的外部jar包的application.properties和application.yml
- 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
总结
指定的环境配置优先,外部优先,后面的可以覆盖前面的同名配置项
SpringBoot原理
SpringBoot启动过程
-
SpringApplication.run(启动类.class, args)
-
new SpringApplication(primarySources).run(args):创建一个SpringApplication并运行
-
SpringApplication的创建
-
保存一些信息
-
获取web应用类型,servlet或webflux
-
bootstrapRegistryInitializers
:获取引导注册初始化器- 从
META-INF/spring.factories
文件中读取org.springframework.boot.BootstrapRegistryInitializer
类型的类
- 从
-
initializers
:获取应用初始化器-
从
META-INF/spring.factories
文件中读取org.springframework.context.ApplicationContextInitializer
类型的类
-
-
listeners
:获取应用监听器-
从
META-INF/spring.factories
文件中读取org.springframework.context.ApplicationListener
类型的类
-
-
deduceMainApplicationClass():推导出main方法所在类
-
-
SpringApplication的运行
-
记录应用启动时间
-
createBootstrapContext
():创建引导上下文- 创建DefaultBootstrapContext对象
- 遍历
bootstrapRegistryInitializers
,调用initialize方法初始化上下文环境设置
-
configureHeadlessProperty():让当前应用进行headless模式,java.awt.headless
-
getRunListeners():获取运行监听器
-
从
META-INF/spring.factories
文件中读取org.springframework.boot.SpringApplicationRunListener
类型的类
-
-
listeners.starting(bootstrapContext, this.mainApplicationClass):遍历上面获取的所有运行监听器,调用
starting
方法,相当于通知关心系统启动事件的监听器系统已启动 -
applicationArguments:保存命令行参数
-
prepareEnvironment():准备环境信息
-
getOrCreateEnvironment():获取或创建一个环境信息
-
configureEnvironment():配置环境信息
1
2
3
4
5
6
7
8
9protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
// 设置类型转换器
environment.setConversionService(new ApplicationConversionService());
}
// 读取所有的配置源的配置属性值
configurePropertySources(environment, args);
configureProfiles(environment, args);
} -
ConfigurationPropertySources.attach(environment):绑定环境信息
-
listeners.environmentPrepared(bootstrapContext, environment):遍历运行监听器调用
environmentPrepared
方法,通知所有运行监听器当前环境已准备完毕
-
-
打印banner
-
createApplicationContext
():创建IOC容器- 根据项目类型(servlet)创建容器
- 当前会创建
AnnotationConfigServletWebServerApplicationContext
-
prepareContext
():准备IOC容器信息- context.setEnvironment(environment):保存环境信息到容器中
- postProcessApplicationContext():IOC容器后置处理
- applyInitializers():应用初始化器
- 遍历创建阶段获取的
ApplicationContextInitializer
,调用initialize方法对IOC容器进行初始化扩展
- 遍历创建阶段获取的
- listeners.contextPrepared(context):遍历运行监听器调用
contextPrepared
方法,通知所有运行监听器IOC容器已准备完毕 - listeners.contextLoaded(context):遍历运行监听器调用
contextLoaded
方法,通知所有运行监听器IOC容器已加载完毕
-
refreshContext
():刷新IOC容器- 创建容器中的所有组件
-
打印启动耗时
-
listeners.started(context, timeTakenToStartup):遍历运行监听器调用
started
方法,通知所有运行监听器当前项目已启动 -
callRunners():调用所有的runner
- 从IOC容器中获取org.springframework.boot.
ApplicationRunner
和org.springframework.boot.CommandLineRunner
类型的runner对象 - 将所有runner按order排序
- 遍历所有runner调用run方法
- 从IOC容器中获取org.springframework.boot.
-
启动成功或失败
- 若启动没有异常
- listeners.ready(context, timeTakenToReady):遍历运行监听器调用
ready
方法,通知所有运行监听器当前项目已准备就绪
- listeners.ready(context, timeTakenToReady):遍历运行监听器调用
- 若启动有异常
- 调用handleRunFailure方法,处理运行失败情况
- isteners.failed(context, exception):遍历运行监听器调用
failed
方法,通知所有运行监听器当前项目启动失败
- isteners.failed(context, exception):遍历运行监听器调用
- 调用handleRunFailure方法,处理运行失败情况
- 若启动没有异常
-
SpringBoot启动过程中的关键组件
- org.springframework.context.
ApplicationContextInitializer
- org.springframework.context.
ApplicationListener
- org.springframework.boot.
SpringApplicationRunListener
- org.springframework.boot.
ApplicationRunner
- org.springframework.boot.
CommandLineRunner
自定义关键组件注意事项
继承对应的接口,实现对应的方法
除了上面的两个runner,其他类都需要在再META-INF/spring.factories
文件中声明
1 |
|
上面的两个runner需要加上@Component
注解注册为组件
1 |
|
1 |
|