SpringBoot 核心功能
yaml配置文件
配置的时候,如果存在多个文件,则优先级为:properties>yml>yaml
基本语法
key:value
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格(IDEA会将tab换成4个空格)
- 缩进的空格数不重要,只要相同层级的元素左对齐可
- '#'表示注释
- ''与""表示字符串内容会被转义()/不转义();常规字符串是否加引号,加''还是加""没有区别
数据类型
字面量:单个的、不可再分的值。date、boolean、string、number、null
1
k:v
对象:键值对的集合。map、hash、set、object
1
2
3
4
5
6k: {k1:v1, k2:v2, k3:v3}
# 或
k:
k1: v1
k2: v2
k3: v3数组:一组按次序排列的值。array、list、queue
1
2
3
4
5
6k: [v1, v2, v3]
# 或
k:
- v1
- v2
- v3
案例
1 |
|
1 | person: |
自定义配置提示
1 | <dependency> |
配置打包时不将其打包:
1 | <build> |
Web开发
SpringMVC自动配置
ContentNegotiatingViewResolver
、BeanNameViewResolver
:内容协商视图解析器和BeanName视图解析器- 静态资源
- 自动注册
Converter
,GenericConverter
, 和Formatter
- 支持
HttpMessageConverters
- 自动注册
MessageCodesResolver
(国际化用) - 静态
index.html
页支持 - 自定义
Favicon
- 自动使用
ConfigurableWebBindingInitializer
不用@EnableWebMvc注解。使用
@Configuration
+WebMvcConfigurer
自定义规则声明
WebMvcRegistrations
改变默认底层组件使用
@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration
全面接管SpringMVC
简单功能分析
静态资源访问
静态资源目录
只要静态资源放在类路径下:
/static
(或/public
或/resources
或META-INF/resources
)访问:当前项目根路径/+静态资源名
原理:静态映射/**
请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面
改变默认的静态资源路径:
1
2
3
4spring:
web:
resources:
static-locations: classpath:abc静态资源访问前缀
默认无前缀,配置前缀:
1
2
3spring:
mvc:
static-path-pattern: "/res/**"访问:当前项目根路径/+static-path-pattern+静态资源名
webjar
自动映射/webjar/**
WebJars - Web Libraries in Jars
例:
1
2
3
4
5<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>可通过:http://127.0.0.1:8080/webjars/jquery/3.5.1/jquery.js访问,后面地址要按照依赖里面的包路径
欢迎页支持
静态资源路径下index.html
可以配置静态资源路径
但不可以配置静态资源的访问前缀,否则导致index.html不能被默认访问
1
2
3spring:
# mvc:
# static-path-pattern: /res/** 这个会导致欢迎页功能失效controller能处理/index
自定义Favicon
favicon.ico放在静态资源目录下即可
1 | spring: |
静态资源配置原理
SpringBoot启动默认加载xxxxAutoConfiguration类(自动配置类),其中会包含与Web开发有关的自动配置类
SpringMVC功能的自动配置类:
WebMvcAutoConfiguration
,生效(条件均满足):1
2
3
4
5
6
7
8
9
10
// 说明通过定义WebMvcConfigurationSupport可以全面接管Spring MVC
public class WebMvcAutoConfiguration {给容器中配置了:
1
2
3
4
5
6
7
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {配置文件的相关属性和什么进行了绑定
- WebMvcProperties:
spring.mvc
- WebProperties:
spring.web
- WebMvcProperties:
配置类只有一个有参构造器,有参构造器所有参数的值都会从容器中确定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// WebProperties webProperties:获取spring.web绑定的所有的值的对象
// WebMvcProperties mvcProperties:获取spring.mvc绑定的所有的值的对象
// ListableBeanFactory beanFactory:Spring的BeanFactroy
// HttpMessageConverters:找到HttpMEssageConverters
// ResourceHandlerRegistrationCustomizer:找到资源处理器的自定义器
// DispatcherServletPath:
// ServletRegistrationBean:给应用注册原生Servlet、Filter、Listener等
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = webProperties.getResources();
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = (ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}资源处理的默认规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
// webjars的规则
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}静态资源默认位置:
1
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
欢迎页处理规则
1
2
3
4
5
6
7
8// HandlerMapping:处理器映射,保存了每个Handler能处理哪些请求
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
请求参数处理
请求映射
rest使用与原理
@xxxMapping
:表示对应的Rest请求Rest风格(使用HTTP请求方式动词来表示对资源的操作)
核心Filter:
HiddenHttpMethodFilter
用法:表单method=post,隐藏域
_method=put
SpringBoot中手动开启
1
2
3
4
5spring:
mvc:
hiddenmethod:
filter:
enabled: true1
2
3
4
5
6
7
8
9
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
修改隐藏域
_method
为自定义名称,如_m
1
2
3
4
5
6
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}Rest原理(表单提交时使用REST风格时)
- 表单提交会带上
_method=PUT
- 请求过来被
hiddenHttpMethodFilter
拦截- 判断请求是否正常,并且是POST请求
- 获取
_method
的值 - 兼容:
PUT
、DELETE
、PATCH
请求 - 对原生request(post),通过装饰器模式requestWrapper重写getMethod方法,返回传入的值
- 过滤器链放行时就用requestWrapper
- 表单提交会带上
如果使用客户端工具发送REST请求则无需filter
请求映射原理
DispatcherServlet继承树:
- HttpServlet
- HttpServletBean
- FrameworkServlet:重写了doGet/doPost等方法,内部调用processRequest方法,processRequest方法再调用doService方法
- DispatcherServlet:重写了doService方法,内部调用doDispatch方法
- FrameworkServlet:重写了doGet/doPost等方法,内部调用processRequest方法,processRequest方法再调用doService方法
- HttpServletBean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 找到当前请求对应的Handler(哪个Controller的哪个方法)
mappedHandler = this.getHandler(processedRequest);1
2
3
4
5
6
7
8
9
10
11
12
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}HandlerMappings
:处理器映射其中:
RequestMappingHandlerMapping
保存了所有@RequestMapping和handle的映射规则所有请求映射都在HandleMapping中
- SpringBoot自动配置了欢迎页的WelcomePageHandlerMapping
- SpringBoot自动配置了默认的RequestMappingHandlerMapping
- 请求进入后逐个尝试所有的HandlerMapping看是否有请求信息
- 如果有就找到这个请求对应的Handler
- 没有就继续找下一个HandlerMapping
- 也可以自定义HandlerMapping
- HttpServlet
普通参数与基本注解
注解:
@PathVariable
:路径参数,路径的{}
中的参数,所有路径参数的集合可用Map@RequestHeader
:请求头,所有请求头的集合可用Map@ModelAttribute
:@RequestParam
:请求参数,集合类型可用List,所有请求参数的集合可用Map@MatrixVariable
:矩阵变量矩阵变量应该绑定在路径变量中
原:/cars/{path}?xxx=xxx&aaa=bbb;queryString查询字符串。@RequestParam
矩阵变量:/cars/path;xxx=xxx;aaa=bbb;矩阵变量
如果Cookie被禁用如何获取Session?
- url重写:/abc;jsessionid=xxxx 把cookie的值使用矩阵变量方式进行传递
SpringBoot需要手动开启矩阵变量功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 继承
public class WebConfig implements WebMvcConfigurer {
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 设置false,表示不移除分号后面的内容,矩阵变量才能生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
// 或者定义Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}例:
1
2
3
public Map carsSell( Integer bossAge,
Integer empAge)
@CookieValue
:获取Cookie,可以用String或Cookie类型参数接收@RequestBody
:获取表单数据@RequestAttribute
:获取Reqeust域属性
Servlet API:
- WebRequest
- ServletRequest
- MultipartRequest
- HttpSession
- javax.servlet.http.PushBuilder
- Principal
- InputStream
- Reader
- HttpMethod
- Locale
- TimeZone
- ZoneId
ServletRequestMethodArgumentResolver可解析以上的部分参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
复杂参数:
- Map, Model:数据会被放在reqeust的请求域,相当于使用
request.setAttribute()
,内部的数据也就可以直接通过request.getAttribute()
获取- Map类型的参数会返回ModelAndViewContainer类中的
getModel()
对应的是BindingAwareModelMap
,它既是Model也是Map - Model类型的参数底层的调用和Map类型一样,并且是同一个对象
- Map类型的参数会返回ModelAndViewContainer类中的
- Errors/BindingResult
- RedirectAttributes:重定向携带数据
- ServletResponse:原生response
- SessionStatus
- UriComponentsBuilder
- ServletComponentsBuilder
自定义对象参数:
- 可以自动类型转换与格式化,可以级联封装
POJO封装过程
- 使用
ServletModelAttributeMethodProcessor
进行解析
参数处理原理
- HandleMapping中找到能处理请求的Handler(哪个Controller中的哪个method)
- 为当前Handler找一个适配器HandlerAdapter,RequestMappingHandlerAdapter
- 通过HandlerAdapter执行目标方法
HandlerAdapter
- RequestMappingHandlerAdapter:支持方法上标注
@RequestMapping
- HandlerFunctionAdapter:支持函数式编程
- HttpReqeustHandlerAdapter
- SimpleControllerHandlerAdapter
执行目标方法
1 | // DispatcherServlet中的doDispatch() |
1 | // RequestMappingHandlerAdapter类中的handleInternal() |
1 |
参数解析器
确定将要执行的目标方法的每个参数值是什么
SpringMVC目标方法能写多少种参数类型就取决于参数解析器
工作步骤:
- 当前解析器是否支持解析这种参数
- 如果支持则调用解析方法(resolveArgument)
返回值处理器
决定能返回哪些类型的值
如何确定目标方法的每一个参数值
1 | // InvocableHandlerMethod类中 |
逐个判断所有参数解析器是否支持解析对应的参数,找到对应的这个解析器,内部判断机制是通过判断是否标注对应的注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// this.resolvers.supportsParameter(parameter)
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
// 缓存
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}解析参数的值
1
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
自定义类型参数,封装POJO
使用ServletModelAttributeMethodProcessor
1
2
3
4
5
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()))); // 判断是否为非简单类型
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory)throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
else {
attribute = ex.getTarget();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
// 外部数据绑定器,将请求参数的值绑定到JavaBean
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}其中重点在于:
1
2// 外部数据绑定器,将请求参数的值绑定到JavaBean(利用converters转换器将请求中的所有数据转换成对应类型后再利用反射绑定到对象)
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);GenericConversionService:在设置每一个值时,找它里面的所有converter中哪个可以将这个数据类型转换到指定的类型
将来就可以给WebDataBinder里面放自己需要的Converter
自定义Converter:
1 |
|
目标方法执行完成
将所有的数据都放在ModelAndViewContainer
中,包含:
- 要去的页面地址View
- Model数据
处理分发结果
1 | processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); |
1 | // |
1 |
|
1 | // 暴露模型作为请求域属性,即将Model中的属性通过request.setAttribute()设置到request域中 |
数据响应与内容协商
响应JSON
jackson.jar+@ResponseBody
在pom.xml中引入
spring-boot-starter-web
时自动引入了json启动器spring-boot-starter-json
,实际上使用的是jackson
包然后就能给前端自动返回JSON数据
返回值解析器
返回值解析器 1
2
3
4
5try {
// 处理返回值
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}1
2
3
4
5
6
7
8
9
10
public void handleReturnValue( Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14// 最终处理返回值
public void handleReturnValue( Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
// 使用消息转换器进行写出操作
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}返回值解析器原理
- 返回值处理器判断是否支持这种类型返回值
supportsReturnType
- 返回值处理器调用
handleReturnValue
进行处理 - RequestResponseBodyMethodProcessor可以处理返回值标了
@ResponseBody
注解的- 利用MessageConverter进行处理,将数据写为json
- 内容协商(浏览器默认会以请求头的方式告诉服务器能接受什么样的内容类型)
- 服务器最终根据自身的能力决定服务器能生产什么样内容类型的数据
- SpringMVC会逐个遍历所有容器底层的HttpMessageConverter,找到能处理的Converter
- 得到MappingJackson2HttpMessageConverter可以将对象写为json
- 利用MappingJackson2HttpMessageConverter将对象转为json再写出去
- 利用MessageConverter进行处理,将数据写为json
- 返回值处理器判断是否支持这种类型返回值
SpringMVC支持哪些返回值
- ModelAndView
- Model
- View
- ResponseEntity
- ResponseBodyEmitter
- StreamingResponseBody
- HttpEntity
- HttpHeaders
- Callable
- DeferredResult
- ListenableFuture
- CompletionStage
- WebAsyncTask
- 有
@ModelAttribute
且为对象类型 - 有
@ResponseBody
,对应处理器为:RequestResponseBodyMethodProcessor
HTTPMessageConverter原理
MessageConverter接口规范
- 判断是否支持此Class类型的对象转为MediaType类型的数据
默认的MessageConverter
- ByteArrayHttpMessageConverter:只支持byte类型
- StringHttpMessageConverter:String类型
- StringHttpMessageConverter:String类型
- ResourceHttpMessageConverter:Resource
- ResourceRegionHttpMessageConverter:ResourceRegion
- SourceHttpMessageConverter:DOMSource.class、SAXSource.class、StAXSource.class、StreamSource.class、Source.class
- AllEncompassingFormHttpMessageConverter:MultiValueMap
- MappingJackson2HttpMessageConverter:全支持,直接返回true
- MappingJackson2HttpMessageConverter:全支持,直接返回true
- Jaxb2RootElementHttpMessageConverter:支持注解方式xml处理
最终MappingJackson2HttpMessageConverter将对象转为JSON(利用底层的jackson的objectMapper进行转换)
内容协商
根据客户端接收能力不同,返回不同媒体类型的数据
引入xml依赖
1
2
3
4<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>postman分别测试返回json和返回xml
只需要改变请求头中Accept字段(application/json或application/xml),HTTP协议规定的,告诉服务器本客户端可以接收的数据类型
开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能
1
2
3
4spring:
mvc:
contentnegotiation:
favor-parameter: true # 开启请求内容协商模式然后就可以通过
http://127.0.0.1:8080/params?format=json
获取json数据;通过http://127.0.0.1:8080/params?format=xml
获取xml数据确定客户端接收什么样的内容类型:
- Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
- 最终进行内容协商返回给客户端json即可
内容协商原理
- 判断当前响应头中是否已经有确定的媒体类型(MedialType)
- 获取客户端支持接收的内容类型。(获取客户端Accept请求头字段):
acceptableTypes = getAcceptableMediaTypes(request);
- contentNegotiationManager:内容协商管理器,默认使用基于请求头的策略
- HeaderContentNegotiationStrategy:确定客户端可以接收的内容类型
- 当设置开启了浏览器参数方式内容协商功能后,就会多一个ParameterContentNegotiationStrategy:基于参数的内容类型协商策略
- 获取服务器可以产生的内容类型;
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
- 遍历循环所有当前系统的MessageConverter,看谁支持操作这个对象
- 找到支持操作Person的converter,把所有converter支持的媒体类型统计出来
- 客户端需要的内容类型(如:application/xml),而服务端的能产生的内容类型是一个列表(多种)
- 进行内容协商,选择最佳匹配媒体类型
- 用对应的converter将对象转为最佳匹配媒体类型
自定义MessageConverter
实现多协议数据兼容
@ResponseBody
响应数据出去调用RequestResponseBodyMethodProcessor处理- Processor处理方法返回值。通过MessageConverter处理
- 所有MessageConverter合起来可以支持各种媒体类型数据的操作(读、写)
- 内容协商找到最终的messageConverter
1 | /** |
1 |
|
有可能添加的自定义功能会覆盖默认很多功能,导致一些默认功能失效,就需要自己添加对应的功能
视图解析与模板引擎
视图解析:SpringBoot默认不支持JSP,需要引入第三方模板引擎技术实现页面渲染
视图解析原理
目标方法处理的过程中,所有数据都会被放在ModelAndViewContainer里面,包括数据和视图地址
方法的参数是一个自定义类型对象(从请求参数中确定的),把它重新放在ModelAndViewContainer
任何目标方法执行完成以后都会返回ModelAndView(数据和视图地址)
processDispatchResult处理分发结果(决定 页面该如何响应)
render(mv, request, response)
进行页面渲染逻辑- 根据方法的String返回值得到View对象(定义了页面的渲染逻辑)
- 所有视图解析器尝试是否能根据当前返回值得到VIew对象
- 根据返回值得到了View,如
redirect:/main.html
--> Thymeleaf视图解析器中new RedirectView() - ContentNegotiationViewResolver里面包含了其它所有的视图解析器,内部还利用其它所有视图解析器得到的视图对象
view.render(mv.getModelInternal(),request,response);
视图对象调用自定义的render进行页面渲染工作- RedirectView如何渲染(重定向到一个页面)
- 获取目标url地址
response.sendRedirect(encodedURL);
- RedirectView如何渲染(重定向到一个页面)
- 根据方法的String返回值得到View对象(定义了页面的渲染逻辑)
视图解析:
- 返回值以
forward:
开始:new InternalResourceView(forwardUrl);
request.getReqeustDispatcher(path).forward(request, response);
:转发 - 返回值以
redirect:
开始:new RedirectView()
render就是重定向 - 返回值是普通字符串:
new ThymeleafView()
模板引擎-Thymeleaf
基本语法
表达式
表达式名字 语法 用途 变量取值 ${...} 获取请求域、session域、对象等值 选择变量 *{...} 获取上下文对象值 消息 #{...} 获取国际化等值 链接 @{...} 生成链接 片段表达式 ~{...} jsp:include作用,引入公共页面片段 字面量
文本值:'some text';数字:0,12;布尔值:true/false
空值:null
变量:a,b,user...
文本操作
字符串拼接:+
变量替换:|The name is ${name}|
数学运算
运算符:+,-,*,/,%
布尔运算
运算符:and, or
一元运算:!, not
比较运算
比较:>,<,>=,<=(gt,lt,ge,le)
等式:==,!=(eq, ne)
条件运算
If-then:(if)?(then)
If-then-else:(if)?(then):(else)
Default: (value)?:(defualtvalue)
特殊操作
无操作:_
设置属性值-th:attr
设置单个值
1
2
3
4
5
6<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>设置多个值
1
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上两个的代替写法
1
2<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">迭代
1
2
3
4
5<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>1
2
3
4
5<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>条件运算
1
2
3<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>1
2
3
4
5<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>属性优先级
1
2
3
4
5
6
7
8
9
10
11```
#### thymeleaf使用
1. 引入starter
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>自动配置的thymeleaf
1
2
3
4
5
public class ThymeleafAutoConfiguration { }自动配置好的策略:
- 所有thymeleaf的配置值都在ThymeleafProperties
- 配置好了SpringTemplateEngine
- 配好了ThymeleafViewResolver
- 使用者只需要直接开发页面
1
2public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; //xxx.html页面开发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<a href="www.atguigu.com" th:href="${link}">去百度</a> <br/>
<a href="www.atguigu.com" th:href="@{/link}">去百度2</a>
</h2>
</body>
</html>
拦截器
案例:
实现HandlerInterceptor接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56/**
* 登录检查为例
*/
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行前
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("拦截的请求路径是{}", requestURI);
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser!=null) return true;
request.setAttribute("msg", "请先登录");
request.getRequestDispatcher("/").forward(request, response);
return false;
}
/**
* 目标方法执行后
* @param request current HTTP request
* @param response current HTTP response
* @param handler the handler (or {@link HandlerMethod}) that started asynchronous
* execution, for type and/or instance examination
* @param modelAndView the {@code ModelAndView} that the handler returned
* (can also be {@code null})
* @throws Exception
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}", modelAndView);
}
/**
* 页面渲染后
* @param request current HTTP request
* @param response current HTTP response
* @param handler the handler (or {@link HandlerMethod}) that started asynchronous
* execution, for type and/or instance examination
* @param ex any exception thrown on handler execution, if any; this does not
* include exceptions that have been handled through an exception resolver
* @throws Exception
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常", ex);
}
}配置拦截器
1
2
3
4
5
6
7
8
9
public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**"); //放行部分请求
}
}
原理
- 根据当前请求,找到HandlerExecutionChain(可以处理请求的Handler以及Handler的所有拦截器)
- 顺序执行所有拦截器的preHandle方法
- 如果当前拦截器preHandle返回true,则执行下一个拦截器的preHandle
- 如果当前拦截器preHandle返回false,则直接倒序执行所有已经执行的拦截器的afterCompletion
- 如果任何一个拦截器返回false,直接跳出,不执行目标方法
- 所有拦截器都返回true,则执行目标方法
- 倒序执行所有拦截器的postHandle方法
- 前面的步骤有任何异常都会直接倒序触发afterCompletion
- 页面成功渲染完成后,也会倒序触发afterCompletiont
文件上传
案例
页面表单
1
2
3
4<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>文件上传代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String upload( MultipartFile headerImg,
MultipartFile[] photos)throws IOException {
if(!headerImg.isEmpty()){
// 保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("F:\\cache\\"+originalFilename));
}
if(photos.length > 0){
for(MultipartFile photo:photos){
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("F:\\cache\\"+originalFilename));
}
}
}
return "main";
}修改相关参数
1
2
3
4
5spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
自动配置原理
文件上传自动配置类:MultipartAutoConfiguration-MultipartProperties
- 自动配置好了
StandardServletMultipartResolver
(文件上传解析器) - 原理步骤:
- 请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 参数解析器解析请求中的文件内容封装成MultipartFile
- 将request中文件信息封装成一个Map;
MultiValueMap<String, MultipartFile>
- FileCopyUtils实现文件流的拷贝
异常处理
默认规则
- 默认情况下,Spring Boot提供
/error
处理所有错误的映射 - 对于机器客户端,它将生成JSON响应,其中包含错误、HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个"whitelabel"错误视图,以HTML格式呈现相同的数据
- 要对其进行自定义,添加View解析为
error
- 要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes
类型的组件以使用现有机制,但替换其内容 - error/下的4xx,5xx页面会被自动解析
定制错误处理逻辑
自定义错误页
- error/404.html、error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找4xx.html;如果都没有就触发白页
@ControllerAdvice+@ExceptionHandler
处理全局异常;底层是ExceptionHandlerExceptionResolver
支持的ErrorViewResolver
实现自定义处理异常实现
HandlerExceptionResolver
处理异常@ResponseStatus+自定义异常
;底层是ResponseStatusExceptionResolver
,把responsestatus
注解的信息Spring底层的异常,如参数转换异常
异常处理自动配置原理
ErrorMvcAutoConfiguration
:自动配置了异常处理规则- 包含的Bean:
DefaultErrorAttributes
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered
- 包含的Bean:
BasicErrorController
(json+白页适配)- 默认处理
/error
请求:@RequestMapping("${server.error.path:${error.path:/error}}")
- 页面响应
new ModelAndView("error", model);
- 容器中有组件View,id是error;(响应默认错误页)
- 容器中放的组件Bean
BeanNameViewResolver
(视图解析器):按照返回的视图名作为组件id去容器中找View对象
- 默认处理
- 包含的Bean:
DefaultErrorViewResolver
- 如果发生错误,会以HTTP状态码作为视图页地址(viewName),找到真正的页面
- error/405、5xx.html
- 包含的Bean:
如果要返回页面,就会找error视图(StaticView)。(默认是一个白页)
异常处理步骤流程
- 执行目标方法,目标方法运行期间有任何异常都会被catch,而且标志当前请求结束;并且用dispatchException
- 进入视图解析流程
mv=processHandlerException;
处理handler发现的异常,处理完成返回ModelAndView- 遍历所有handlerExceptionResolvers,看哪个能处理当前异常(HandlerExceptionResolver处理器异常解析器)
- 系统默认的异常解析器
- DefaultErrorAttributes先来处理异常,把异常信息保存到request域,并且返回null
- 默认没有任何类能处理异常,所以异常会被抛出
- 如果没有能处理最终底层就会发送/error请求,会被底层的BasicErrorController处理
- 解析错误视图;遍历所有的ErrorViewResolver看哪个能解析
- 默认的DefaultErrorViewResolver,作用是把响应状态码作为错误页的地址,error/500.html
- 模板引擎最终响应这个页面error/500.html
Web原生组件注入(Servlet、Filter、Listener)
1. 使用Servlet APi
@ServletComponentScan(basePackages = "com.atguigu.admin")
:指定原生Servlet组件都放在哪里@WebServlet(urlPatterns = "/my")
:效果:直接响应,没有经过Spring的拦截器@WebFilter(urlPatterns={"/css/*","/images/*"})
:拦截指定路径的请求@WebListener
DispatcherServlet如何注册?
- 容器中自动配置了
DispatcherServlet
属性,绑定到WebMvcProperties
;对应的配置文件配置项是spring.mvc
- 通过
ServletRegistrationBean<DispatcherServlet>
把DispatcherServlet配置进来 - 默认映射的是/路径
Tomcat处理Servlet时,如果多个Servlet都能处理到同一层路径,精确优先原则,如:
DispatcherServlet: /;MyServlet:/my,则当请求是/my时就不会通过DispatcherServlet,而是直接通过MyServlet,也就不会经过Spring流程,这是上述原生Servlet不经过拦截器的原因
2. 使用RegistrationBean
1 |
|
嵌入式Servlet容器
1. 切换嵌入式Servlet容器
默认支持的webServer
- Tomcat、Jetty、Undertow
- ServletWebServerApplicationContext容器启动寻找ServletWebServerFactory并引导创建服务器
切换服务器
1
2
3
4
5
6
7
8
9
10<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
原理:
- SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
- web应用会创建一个web版的ioc容器
ServletWebServerApplicationContext
ServletWebServerApplicationContext
启动的时候寻找ServletWebServerFactory
(Servlet 的web服务器工厂---> Servlet 的web服务器SpringBoot底层默认有很多的WebServer工厂;
TomcatServletWebServerFactory,
JettyServletWebServerFactory, or
UndertowServletWebServerFactory`底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
内嵌服务器,就是手动调用启动服务器的代码(tomcat核心jar包存在)
2. 定制Servlet容器
- 实现
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
接口- 把配置文件的值和
ServletWebServerFactory
进行绑定
- 把配置文件的值和
- 修改配置文件
server.xxx
- 直接自定义
ConfigurableServletWebServerFactory
1 | import org.springframework.boot.web.server.WebServerFactoryCustomizer; |
定制化原理
定制化常见方式
修改配置文件
xxxxCustomizer:定制化器,可以修改xxxx的默认规则
编写自定义的配置类xxxConfiguration;+@Bean替换、增加容器中默认组件;视图解析器
Web应用:编写一个配置类实现WebMvcConfigure即可定制化web功能;+@Bean给容器中再扩展一些组件
@EnableWebMvc
+WebMvcConfigure—@Bean可以全面接管SpringMVC,所有规则全部自己重新配置;实现定制和扩展功能原理
1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。配置静态资源、欢迎页等
2、一旦使用 @EnableWebMvc 。就会 @Import(DelegatingWebMvcConfiguration.class)
3、而DelegatingWebMvcConfiguration ,只能保证SpringMVC最基本的使用
- 将所有系统中的
WebMvcConfigurer
拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效 - 自动配置了一些非常底层的组件。
RequestMappingHandlerMapping
,这些组件依赖的组件都是从容器中获取public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
- 将所有系统中的
4、WebMvcAutoConfiguration 里面的配置要能生效 必须满足@ConditionalOnMissingBean(WebServerFactoryCustomizer.WebServerFactoryCustomizer)
5、
@EnableWebMvc
导致了 WebMvcAutoConfiguration 没有生效。
原理分析套路
场景starter->xxxxAutoConfiguration->导入xxx组件->绑定xxxProperties->绑定配置文件项
数据访问
1. SQL
HikariDataSource数据源的自动配置
导入JDBC场景
1 | <dependency> |
- 包括:HikariPC数据源、JDBC、事务相关(spring-tx)
没有数据库驱动,为什么?
因为不确定要使用什么数据库,因此需要人为配置,默认版本是:<mysql.version>8.0.22</mysql.version>
分析自动配置
DataSourceAutoConfiguration
:数据源的自动配置- 修改数据源相关的配置:
spring.datasource
- 数据库连接池的配置,当容器中没有配置DataSource时才自动配置
- 底层配置好的连接池是:HikariDataSource
- 修改数据源相关的配置:
DataSourceTransactionManagerAutoConfiguration
:事务管理器的自动配置JdbcTemplateAutoConfiguration
:JDBCTemplate的自动配置,可以对数据库进行CRUD- 可以修改
@ConfigurationProperties(prefix="spring.jdbc")
来修改JdbcTemplate - 容器中已经有了
JdbcTemplate
组件
- 可以修改
JndiDataSourceAutoConfiguration
:Jndi的自动配置XADataSourceAutoConfiguration
:分布式事务相关的
修改配置项
1 | spring: |
使用Druid数据源
自定义方式
创建数据源
1
2
3
4
5<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />
</bean>或者自定义配置类
1
2
3
4
5
6
public DataSource dataSource(){
DataSource dataSource = new DruidDataSource();
return dataSource;
}StatViewServlet
StatViewServlet的用途包括:
- 提供监控信息展示的html页面
- 提供监控信息的JSON API
配置Servlet
1
2
3
4
5
6
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<StatViewServlet>(statViewServlet, "/druid/*");
return registrationBean;
}StatFilter
用于统计监控信息;如SQL监控、URI监控
1
2
3
4
5
6
7
public DataSource dataSource() throws SQLException {
DataSource dataSource = new DruidDataSource();
dataSource.setFilters("stat"); // 加入监控功能
return dataSource;
}系统中的所有Filter:
别名 Filter类名 default com.alibaba.druid.filter.stat.StatFilter stat(监控页) com.alibaba.druid.filter.stat.StatFilter mergeStat com.alibaba.druid.filter.stat.MergeStatFilter encoding com.alibaba.druid.filter.encoding.EncodingConvertFilter log4j com.alibaba.druid.filter.logging.Log4jFilter log4j2 com.alibaba.druid.filter.logging.Log4j2Filter slf4j com.alibaba.druid.filter.logging.Slf4jLogFilter commonlogging com.alibaba.druid.filter.logging.CommonsLogFilter 慢SQL记录配置:
1
2
3
4
5
6
7
public StatFilter statFilter(){
StatFilter statFilter = new StatFilter();
statFilter.setSlowSqlMillis(10000);
statFilter.setLogSlowSql(true);
return statFilter;
}
使用官方starter方式
1 | <dependency> |
分析自动配置:
- 扩展配置项
spring.datasource.druid
- DruidSpringAopConfiguration.class, 监控Spring Bean的;配置项:
spring.datasource.druid.aop-patterns
- DruidStatViewServletConfiguration.class, 监控页的配置:
spring.datasource.druid.stat-view-servlet
;默认开启 - DruidWebStatFilterConfiguration.class, web监控配置;
spring.datasource.druid.web-stat-filter;
默认开启 - DruidFilterConfiguration.class 所有Druid自己filter的配置
配置示例:
1 | spring: |
整合MyBatis
依赖:
1 | <dependency> |
1. 配置模式
- 全局配置文件
- SqlSessionFactory:自动配置好了
- SqlSession:自动配置了SqlSessionTemplate组合了SqlSession
- Mapper:只需要写的操作MyBatis的接口标注了
@Mapper
就会被自动扫描(@Import(AutoConfiguredMapperScannerRegistrar.class
)
1 | // MyBatis配置项绑定类。 |
1 | // 相关配置的对应前缀 |
配置:
- 配置
mybatis.configuration
下面的配置就相当于改mybatis全局配置文件中的值
1 | # 配置mybatis规则 |
流程:
- 导入mybatis官方starter
- 编写mapper接口,并标注
@Mapper
注解 - 编写sql映射文件并绑定mapper接口
- 在application.yaml中指定mapper配置文件的位置,并配置全局配置信息(
mybatis.configuration
)
2. 注解模式
1 |
|
3. 混合模式
1 |
|
流程:
- 导入mybatis官方starter
- 配置application.yaml,指定
mapper-location
- 编写Mapper接口并标注
@Mapper
注解 - 简单方法直接注解方式
- 复杂方法编写mapper.xml进行绑定映射
@MapperScan("com.zephon.admin.mapper")
简化,其它接口就不用标注@Mapper
注解
整合MyBatis-Plus
导入依赖
1
2
3
4
5<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>继承接口/类
1
2
3
4
5
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
}1
2
3public interface UserService extends IService<User> {
}
2. NoSQL
Redis自动配置
1 | <dependency> |
自动配置:
- RedisAutoConfiguration自动配置类。RedisProperties属性类 -->
spring.redis.xxx
是对redis的配置 - 连接工厂是准备好的。
LettuceConnectionConfiguration
、JedisConnectionConfiguration
- 自动注入了
RedisTemplate<Object, Object>
:xxxTemplate - 自动注入了
StringRedisTemplate
,k:v都是String - 底层只要使用
StringRedisTemplate
、RedisTemplate
就可以操作Redis
例:
1 |
|
切换到jedis客户端
1 | <dependency> |
配置:
1 | spring: |
JUnit5单元测试
Junit5的变化
JUnit5=Junit Platform + JUnit Jupiter + Junit Vintage
JUnit Platform:是在JVM上启动测试框架的基础,不仅支持JUnit自制的测试引擎,其它测试引擎也都可以接入
JUnit Jupiter:提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于JUnit Platform上运行
JUnit Vintage:由于JUnit已经发展多年,为了照顾老项目,JUnit Vintage提供了兼容JUnit4.x,JUnit3.x的测试引擎
注:SpringBoot2.4以上版本移除了默认对Vintage的依赖,如果需要兼容JUnit4需要自行引入,否则不能使用JUnit4的功能
@Test
对应依赖
1 | <dependency> |
使用方法:
1 |
|
JUnit5常用注解
@Test
:表示方法是测试方法@ParameterizedTest
:表示方法是参数化测试@RepeatedTest
:表示方法可重复执行@DisplayName
:为测试类或者测试方法设置展示名称@BeforeEach
:表示在每个单元测试之前执行@AfterEach
:表示在每个单元测试之后执行@BeforeAll
:表示在所有单元测试之前执行@AfterAll
:表示在所有单元测试之后执行@Tag
:表示单元测试类别@Disabled
:表示测试类或测试方法不执行@Timeout
:表示测试方法运行如果超过了指定时间将会返回错误@ExtendWith
:为测试类或测试方法提供扩展类引用- 更多
断言(assertions)
1. 简单断言
用来对单个值进行简单的验证。如:
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
1 |
|
2. 数组断言
通过assertArrayEquals方法来判断两个对象或原始类型的数组是否相等
1 |
|
3. 组合断言
assertAll方法接受多个org.junit.jupiter.api.Executable函数式接口的实例作为要验证的断言,可以通过lambda表达式提供这些断言
1 |
|
4. 异常断言
JUnit5提供一种新的断言方式Assertions.assertThrows()
配合函数式编程就可以进行使用
1 |
|
5. 超时断言
Junit5提供了Assertions.assertTimeout()
为测试方法设置超时时间
1 |
|
6. 快速失败
通过fail方法直接使得测试失败
1 |
|
前置条件(assumptions)
JUnit5中的前置条件(assumptions(假设))类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要
1 |
|
- assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。
- assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
嵌套测试
JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试。
在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。
1 |
|
参数化测试
参数化测试是JUnit5很重要的一个新特性,它使得不同的参数多次运行测试成为可能,也为单元测试提供很多便利。
利用@ValueSource
等注解,指定入参,将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource
:为参数化测试指定入参来源,支持八大基础类和String类型、Class类型@NullSource
:表示为参数化测试提供一个null的入参@EnumSource
:表示为参数化测试提供一个枚举入参@CsvFileSource
:表示读取指定csv文件内容作为参数化测试入参MethodSource
:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
此外,还可以支持外部的各类入参如CSV、YML、JSON文件甚至方法的返回值也可以作为入参。只需要实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。
1 |
|
JUnit4到JUnit5迁移指南
在进行迁移的时候需要注意如下的变化:
- 注解在 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
1、简介
每个微服务在云上部署之后,都需要对其进行监控、追踪、审计和控制等。SpringBoot抽取了Actuator场景,使得每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
1 | <dependency> |
2、如何使用
引入场景
访问
http://localhost:8080/actuator/**
暴露所有监控信息为HTTP
1
2
3
4
5
6management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露测试
http://localhost:8080/actuator/beans
http://localhost:8080/actuator/configprops
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/jvm.gc.pause
http://localhost:8080/actuator/endpointName/detailPath
......
3、可视化
https://github.com/codecentric/spring-boot-admin
Actuator Endpoint
1、常用端点
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 |
执行线程转储。 |
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:日志记录
2、Health Endpoint
健康检查端点,一般用于在云平台会定时检查应用的健康状况,就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合
- Health Endpoint返回的结果是一系列健康检查后的一个汇总报告
- 很多的健康检查默认是自动配置好了的,如:数据库、Redis等
- 可以很容易的添加自定义的健康检查机制
3、Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或push(被动获取)方式得到
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或扩展已有Metrics
4、管理Endpoints
开户与禁用Endpoints
默认所有Endpoint除了shutdown都是开启的
需要开启或禁用某个Endpoint,配置模式为
management.endpoint.<endpointName>.enabled=true
1
2
3
4management:
endpoint:
beans:
enabled: true或禁用所有的Endpoint然后手动开启指定的Endpoint
1
2
3
4
5
6
7
8management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
暴露Endpoints
支持的暴露方式:
HTTP:默认只暴露health和info Endpoint
JMX:默认暴露所有Endpoint
除了Health和info,剩下的Endpoint都应该进行保护访问,如果引入SpringSecurity,则会默认配置安全访问规则
ID JMX Web auditevents
Yes No beans
Yes No caches
Yes No conditions
Yes No configprops
Yes No env
Yes No flyway
Yes No health
Yes Yes heapdump
N/A No httptrace
Yes No info
Yes Yes integrationgraph
Yes No jolokia
N/A No logfile
N/A No loggers
Yes No liquibase
Yes No metrics
Yes No mappings
Yes No prometheus
N/A No scheduledtasks
Yes No sessions
Yes No shutdown
Yes No startup
Yes No threaddump
Yes No
定制Endpoint
1、定制Health信息
实现HealthIndicator
接口:
1 | import org.springframework.boot.actuate.health.Health; |
1 | # management.endpoint.端点名.xxxx:对某个端点的具体配置 |
或继承抽象类AbstractHealthIndicator
:
1 |
|
2、定制info信息
常用两种方式:
编写配置文件
1
2
3
4
5info:
appName: boot-admin # 自定义key: value
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@编写InfoContributor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import java.util.Collections;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;
public class ExampleInfoContributor implements InfoContributor {
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}
}
3、定制Metrics信息
SpringBoot支持自动适配的Metrics
定制Metrics
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter"); // 指标注册
}
public void hello() {
counter.increment();
}
}
//也可以使用下面的方式
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
4、定制Endpoint
1 |
|
原理解析
Profile功能
为了方便多环境适配,springboot简化了profile功能
1、application-profile功能
- 默认配置文件
appliction.yaml
任何时候都会加载 - 指定环境配置文件
application-{env}.yaml
- 激活指定环境
- 配置文件激活:
spring.profiles.active=test
- 命令行激活:
--spring.profiles.active=test
- 可以进一步修改其中的配置
--spring.profiles.active=test --server.port=9000
- 可以进一步修改其中的配置
- 配置文件激活:
- 默认配置与环境配置同时生效
- 同名配置项,profile配置优先
2、@Profile条件装配功能
1 |
|
3、profile分组
1 | proddb = |
使用:--spring.profiles.active=production 激活
外部化配置
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
1、外部配置源
常用:Java属性文件、YAML文件、环境变量、命令行参数
2、配置文件查找位置
- classpath根路径
- classpath根路径下config目录
- jar包当前目录
- jar包当前目录的config目录
- /config子目录的直接子目录
优先级自下往上
3、配置文件加载顺序
- 当前jar包内部的application.properties和application.yml
- 当前jar包内部的application-{profile}.properties和application-{profile}.yml
- 引用的外部jar包的appliction.properties和appliction.yml
- 引用的外部jar包的application-{profile}.properties和application-{profile}.yml
总结:
指定环境优先,外部优先,后面的可以覆盖前面的同名配置项
自定义starter
1、starter启动原理
- starter-pom引入自定义的autoconfigure包
- autoconfigure包配置使用META-INF/spring.factories中EnableAutoConfiguration的值,使得项目启动加载指定的自动配置类
- 编写自动配置类xxxAutoConfiguration -> xxxProperties
@Configuration
@Conditional
@EnableConfigurationProperties
@Bean
- ......
引入starter ---> xxxAutoconfiguration ---> 容器中放入组件 ----> 绑定xxxProperties ---> 配置项
2、自定义starter
hello-spring-boot-starter(启动器):在pom.xml中引入自定义的自动配置包
在类路径创建META-INF/spring.factories文件,配置:
1 | com.abc.hello.auto.HelloServiceAutoConfiguration = |
hello-spring-boot-starter-autoconfigure(自动配置包)
hello.bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}hello.service
1
2
3
4
5
6
7
8
9
10
11/**
* 默认不要放在容器中
*/
public class HelloService {
HelloProperties helloProperties;
public String sayHell(String username) {
return helloProperties.getPrefix() + ":" + username + "->" + helloProperties.getSuffix();
}
}hello.auto.HelloServiceAUtoConfiguration
1
2
3
4
5
6
7
8
9
// 默认HelloProperties放在容器中
public class HelloServiceAutoConfiguration {
public HelloService helloService(){
return new HelloService();
}
}
SpringBoot原理
SpringBoot启动过程
- 创建SpringApplication
- 保存一些信息
- 判断当前应用的类型(Servlet)。
ClassUtils
- bootstrappers:初始启动引导器(
List<Bootstrapper>
):去spring.factories
文件中找org.springframework.boot.Bootstrapper
- 找
ApplicationContextInitializer
,去spring.factories
找ApplicationContextInitializer
List<ApplicationContextInitializer<?> initializers
- 找
ApplicationListener
应用监听器,去spring.factories
找ApplicationListener
List<ApplicationListener<?>> listeners
- 运行SpringApplication
- StopWatch
- 记录应用的启动时间
- 创建引导上下文(Context环境)
createBootstrapContext()
- 获取到之前所有的bootstrappers,逐个执行
initialize()
来完成对引导启动器上下文环境设置
- 获取到之前所有的bootstrappers,逐个执行
- 让当前应用进入
headless
模式,java.awt.headless
- 获取所有
RunListener
(运行时监听器) (为了方便所有Listener进行事件感知)getSpringFactoriesInstances
:去spring.factories
找SpringApplicationRunListener
- 遍历
SpringApplicationRunListener
调用starting方法 - 这个过程相当于通知所有感兴趣系统正在启动过程的监听器
- 保存命令行参数:
ApplicationArguments
- 准备环境:
prepareEnvironment()
- 返回或创建基础环境信息对象,
StandardServletEnvironment
- 配置环境信息对象
- 读取所有的配置源的配置属性值
- 绑定环境信息
- 监听器调用
listener.enviromentPrepared()
通知所有监听器当前环境准备完成
- 返回或创建基础环境信息对象,
- 创建IOC容器(createApplicationContext())
- 根据项目类型(Servlet)创建容器
- 当前会创建
AnnotationConfigServletWebServerApplicationContext
- 准备IOC容器(
ApplicationContext
)的基本信息,prepareContext()
- 保存环境信息
- IOC容器的后置处理流程
- 应用初始化器(
applyInitializers
)- 遍历所有
ApplicationContextInitializer
,调用initialize
,来对ioc容器进行初始化扩展功能 - 遍历所有
listener
调用contextPrepared
,(EventPublishRunListener
);通知所有的监听器contextPrepared
完成
- 遍历所有
- 所有的
listener
调用contextLoaded
,通知所有的监听器contextLoaded
完成
- 刷新IOC容器(
refreshContext
)- 创建容器中的所有组件(Spring注解)
- 容器刷新完成后工作
afterRefresh
- 所有监听器调用
listeners.started(context)
通知所有的监听器started - 调用所有runners;
callRunners()
- 获取容器中的
ApplicationRunner
- 获取容器中的
CommandLineRunner
- 合并所有runner并且按照
@Order
进行排序 - 遍历所有runner,调用run方法
- 获取容器中的
- 如果以上有异常则调用
listener
的failed
- 调用所有监听器的running方法
listeners.running(context)
通知所有的监听器running - running如果有问题,继续通知failed,调用所有Listener的failed,通知所有监听器failed