SpringBoot总结

SpringBoot总结

一、静态资源处理

1.1 静态资源访问

    静态资源访问过程中,springBoot默认访问存储路径下的/static (or /public or /resources or /META-INF/resources)

但是可以使用SpringBoot配置文件中application.properties 或者 application.yaml 配置如下:

  1. 静态资源默认将所有的请求/** 进行拦截,但是为了方便拦截器对其进行拦截,统一为静态资源设置前缀:
1
2
spring.mvc.static-path-pattern:/resources/**
resources代表你需要映射的访问静态资源路径的前缀
  1. 设置静态资源存储的路径:
1
2
spring.web.resources.static-locations:classpath:[/resourcesSta/]
表示将静态资源存储到类路径下的:resourcesSta文件夹内,可以配置多个,数组写法
  1. 访问webjar类型

    webjar官网
        webjar实际上是将其类似js等文件封装成jar包格式,将其使用maven导入之后,访问路径为:localhost:端口号/webjar/xxx/xxx即可以访问。

静态资源访问原理:

1.2 欢迎页和favicon(浏览器小图标)

1.2.1 静态页面作为欢迎页

    将其欢迎页面(index.html)存储到静态资源路径下,即可进行访问,可以配置静态资源实际存储路径。但是在配置静态资源访问前缀之后,默认的index.html页面将会失效,无法访问!

1.2.2 favicon图标

    将其放到静态资源路径上面即可以自动识别,但是名称必须要为favivon.xxx若配置静态资源访问前缀,则其也会失效!

1.3 静态资源处理源码分析

1.3.1 静态资源处理源码与配置文件映射

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
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware

构造方法:从容器中拿到:
webProperties:资源文件属性 spring.web配置文件中取到
mvcProperties:mvc配置文件属性从 spring mvc配置文件中取到
beanFactory:spring容器工厂
messageConvertersProvider:消息类型转化器
resourceHandlerRegistrationCustomizer:资源处理器的自定义器
dispatcherServletPath:spring的servlet
servletRegistrations:原生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 = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}

1.3.2 资源映射处理的方法:

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
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}

private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
Consumer<ResourceHandlerRegistration> customizer) {
if (registry.hasMappingForPattern(pattern)) {
return;
}
ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
customizer.accept(registration);
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));//注册配置的静态资源缓存时间
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
customizeResourceHandlerRegistration(registration);
}
1
2
3
spring.web.resources.cache 相关的缓存配置:
配置静态资源缓存:告诉浏览器静态资源需要存储的时间
spring.web.resources.cache.period:秒

1.3.3 欢迎页的静态资源访问源码

handlerMapping:保存每一个Handler能处理哪种请求。

    依旧是在WebMvcAutoConfiguration中,找到WelcomePageHandlerMapping 方法:其实际上向容器中放置了一个组件,其负责处理欢迎页的请求。传入参数中this.mvcProperties.getStaticPathPattern() 映射的就是静态资源访问路径。

1
2
3
4
5
6
7
8
9
10
11
WebMvcAutoConfiguration:中的私有方法向容器中注入一个处理欢迎页的请求映射handler
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}

进入WelcomePageHandlerMapping中:

参数:

  1. staticPathPattern:为自定义静态资源访问路径
  2. welcomePage:找到的欢迎页
    1. welcomePageHandlerMapping 传入的参数中的welcomPage(由getWelcomePage()方法获得)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
WebMvcAutoConfiguration:中的私有方法
private Resource getWelcomePage() {
for (String location : this.resourceProperties.getStaticLocations()) {
Resource indexHtml = getIndexHtml(location);
if (indexHtml != null) {
return indexHtml;
}
}
ServletContext servletContext = getServletContext();
if (servletContext != null) {
return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
return null;
}

getWelcomePage 方法可以发现,其遍历寻找在静态资源路径下文件,看起是否存在index.html 如果存在就传入一个不为空的welcomPage文件。

1
2
3
4
5
6
7
8
9
10
11
12
WelcomePageHandlerMapping类的构造方法:
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}

这里再判断欢迎页是否存在,以及其是否在/**目录下,这里当自定义静态资源路径之后,由于这里的welconPage是由**WebMvcAutoConfiguration中的getWelcomePage()方法再静态资源路径下查找获得的 因此其并不匹配if 判断的存在与****/****** 目录下,因此会返回空白页!这也就是上面配置静态资源路径后index.html无法出现的原因!**

if条件不成立之后,会返回视图名称****index 查看哪个controller能处理这个视图。

1
2
3
4
5
6
7
8
WelcomePageHandlerMapping:私有方法
setRootViewName()方法
private void setRootViewName(String viewName) {
ParameterizableViewController controller = new ParameterizableViewController();
controller.setViewName(viewName);
setRootHandler(controller);
setOrder(2);
}

1.3.4 favicon

浏览器默认会发送,项目路径下的/favicon.ico 静态资源,当修改静态资源访问路径之后,就无法找到这个文件,因此也会出现图标空白的情况。

二、请求参数处理

2.1 Rust 风格源码解析

2.1.1 Rust 风格原理

在SpringBoot自动配置类中,默认配置了一个OrderedHiddenHttpMethodFilter 类,其实际上继承HiddenHttpMethodFilter 。在HiddenHttpMethodFilter 这个doFilterInternal()方法中,负责将请求方法首次进行封装,通过其中的_method 参数获取真正请求方式post、get、delete、put ,再将其request 中的请求方法进行替换,再次封装,传递给下一层进行请求被业务处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HiddenHttpMethodFilter类中的方法:	

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

HttpServletRequest requestToUse = request;

if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
String paramValue = request.getParameter(this.methodParam);//获取隐藏的真正的请求
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
//再次包装request,这里构造方法调用父类方法,也就是重写父类的请求方法
}
}
}

filterChain.doFilter(requestToUse, response);
}

    但是虽然自动配置注入了分发请求的Rest风格的Filter,但是观察自动配置文件中的配置方法可以知道,在@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled") 配置文件中这一项spring.mvc.hiddenmethod.filter 开启之后才会启用Rest风格的请求。

1
2
3
4
5
6
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}

开启Rust风格实际上,这个配置spring.mvc.hiddenmethod.filter 只需要在做页面开发的时候需要开启,实际上原生是支持post、get、delete、put 这些请求参数的(用postMan等第三方不涉及页面表单都可以兼容)。

2.1.2 自定义请求_method ``定制化SpringBoot

当然也可以自定义,这个参数method* ,因为实际上其,是因为容器中不存在HiddenHttpMethodFilter.class 才将其放入容器,又因为其实质上还是调用的HiddenHttpMethodFilter.class中的doFilterInternal()方法实现请求方式在包装的,因此直接可以给容器中放一个HiddenHttpMethodFilter 来修改其默认的参数*method

1
2
3
4
5
6
7
8
9
@Bean
@Configuration(proxyBeanMethods = false)//多例模式proxyBeanMethods true表示再次调用这些@Bean的方法会从容器中寻找
public class WebConfig implements WebMvcConfigurer {
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}

2.2 请求参数注解

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie)
@PostMapping("/save")
public Map postMethod(@RequestBody String content)

集合注入为,全部参数或者全部属性到List中或者Map

  1. @PathVariable 路径变量 全部一次性取出Map<String,String>
  2. @RequestHeader 获取请求头参数 多个值注入Map<String,String>
  3. @RequestParam 获取请求参数(比如表单提交的)支持集合注入,一个属性对应多个值
  4. @CookieValue 获取Cookie
  5. @RequestBody 获取请求体
  6. @RequestAttribute 获取Request域内的属性参数
  7. @MatrixVariable 矩阵变量 必须要有路径变量才能被解析
    1. value属性:指定其取出的V是哪个KEY的
    2. pathVar属性:表示取出哪一个路径变量后的矩阵变量
      请求路径:/cars/sell url路径:/cars/sell;low=34;brand=byd,audi,yd 每个参数之间用;隔开。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1
所有矩阵变量参数在一个路径变量的后面
请求路径:/cars/sell;low=34;brand=byd,audi,yd
@GetMapping("/cars/{path}")
public Map carsSell(@PathVariable("path") String path,
@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand)
path = sell low = 34 brand=[byd,audi,yd]
----------------------------------------------
----------------------------------------------
2
矩阵变量存在不同的路径变量后面
请求路径:/boss/1;age=20/2;age=10
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge)
bossAge = 20 empAge = 10

开启矩阵变量需要,定制化修改SpringBoot:在WebMvcAutoConfigration下存在一个自动配置的configurePathMatch,其中使用urlPathHelper(处理请求路径)其中含有一个属性removeSemicolonContent(意思为移出分号后内容,也就是请求路径上分号后面内容全部移出。)

定制化放入一个configurePathMatch

Text
1
2
3
4
5
6
7
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除;后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}

2.3 DispatherServlet

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
70
71
72
73
74
75
76
77
78
79
80
81
82
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;//判断是否多文件上传请求

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);//看请求是否存在异步

try {
ModelAndView mv = null;//初始化空视图
Exception dispatchException = null;//用来捕获处理请求过程中的异常

try {
processedRequest = checkMultipart(request);//检查是否文件上传请求,进行转化
multipartRequestParsed = (processedRequest != request);//Boolean类型 是否文件上传

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);//决定哪个handler来处理请求
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

2.3.1 mappedHandler = getHandler(processedRequest)

1
2
3
4
5
6
7
8
9
10
11
12
@Nullable
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;
}

    SpringMVC决定使用哪一个handler进行处理请求,通过这里的handlerMapping 获取所有的请求映射,使用遍历,看谁能够处理。SpringBoot自动配置了RequestMappingHandlerMapping WelcomePageHandlerMappingWebMvcAutoConfiguration

image

image

执行流程:

2.4 系统参数解析原理以及自定义POJO参数解析

2.4.1 系统参数解析原理

2.4.2 POJO自定义参数解析

将请求注入到,自定义对象POJO中,级联属性可以直接封装,但是其他类似2.4.2.3这种自定义类型就需要自定义转化器Converter进行转化,才能够支持。

2.4.2.1 自动封装POJO(级联属性)方式

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
例子:
bean对象
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private Integer age;
}
控制器:
@PostMapping("/saveuser")
public Person saveuser(Person person){
return person;
}
表单请求:级联封装 >>>> 对象属性.内部对象属性
<form action="/saveuser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
宠物姓名:<input name="pet.name" value="阿猫"/><br/>
宠物年龄:<input name="pet.age" value="5"/>
<input type="submit" value="保存"/>
</form>

2.4.2.2 自动封装POJO 原理(数据绑定器binder–>>转化器Converters)

自定义类型参数封装,使用的还是通过HandlerMethodArgumentResolverComposite 找到参数解析器,最后返回参数解析器,而在自定义对象封装使用的是ServletModelAttributeMethodProcessor****参数解析器,最后通过参数解析器的args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);方法进行解析参数。

image

ServletModelAttributeMethodProcessor参数解析器判断是否支持的方法:resolver.supportsParameter(parameter)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
调用ModelAttributeMEthodProcessor类中supportsParameter方法:
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
------------------------------
------------------------------
BeanUtils.isSimpleProperty:实际上就是判断是不是简单类型
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}

解析流程:

创建一个WebBinder数据绑定器,将其请求数据以及创建的空对象绑定到绑定器中,之后通过绑定器中包含的转换服务中的各种转换器将其数据类型进行转化,最终通过反射注入到JavaBean中

实际执行数据绑定封装关键步骤:创建数据绑定器之后执行bindRequestParameters(binder, webRequest); 进行绑定,再绑定过程中,获取原生类型,将binder转化为原生请求类型的binder,之后获取原生请求的参数,然后检查哪些需要绑定,之后通过遍历属性,一个一个获得之后通过反射器反射注入参数,设置值的过程中使用类型转化器(conventer)进行转化,最后执行提交。执行结束之后,数据成功被封装到JavaBean当中。

image

2.4.2.3 自定义Converter参数绑定 定制化SpringBoot

1
2
3
4
5
6
7
8
例子:自定义协议-->>pet属性使用;隔开前面名字后面年龄
<form action="/saveuser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
宠物: <input name="pet" value="啊猫,3"/>
<input type="submit" value="保存"/>
</form>

还是在WebMvcAutoConfiguration中配置:addFormatters 添加格式化器Formatters或者类型转化器Converters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class WebConfig  implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
//Converter<S, T> 原类型--->目标类型
registry.addConverter(new Converter<String, Pet>() {
@Override
//重写方法
public Pet convert(String source) {
// 啊猫,3
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
}

三、响应处理与内容协商

四、视图解析模板引擎

五、拦截器

六、文件上传

七、异常处理

八、原生组件Filter、Servlet、Listener注入

8.1 DispatcherServlet 底层注入方法

springBoot底层对于DispatcherServlet存在自动配置类:org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration

image

底层对DispatcherServlet进行注入,并且绑定配置类WebMvcProperties.class(SpringMvc配置类)–>绑定配置文件前缀prefix = “spring.mvc”,然后还注入了一个文件上传解析器

image

底层将DispatcherServlet注入Spring容器:采用的还是DispatcherServletRegistrationConfiguration实质上采用的就是ServletRegistrationBean这种方式进行注册。

image

8.2 使用Servlet原生提供的API

  1. 在Spring配置类上提供Servlet原生组件的存放包注解 @ServletComponentScan(basePackages = “com.atguigu.admin”)
  2. 在原生组件的上面使用以下注解进行标注并继承对应的HttpServlet、实现对应的Filter、实现对应的ServletContextListener
    1. @WebServlet(urlPatterns = “/my” …..) 会直接进行响应,不会通过DispatcherServlet进行分发,因为路径为精准匹配,有精准先精准
    2. @WebFilter(urlPatterns={“/css/*“,“/images/*“})
    3. @WebListener listener会在Servlet容器启动时进行运行contextInitialized,关闭时进行运行contextDestroyed

8.3 使用Spring提供的进行注册

这里最好保证为单实例,否则会出现注入新的servlet导致组件冗余

image

8.3.1 ServletRegistrationBean

  1. 声明一个配置类
  2. 注入一个ServletRegistrationBean
1
2
3
4
5
6
7
8
9
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();

return new ServletRegistrationBean(myServlet,"/my","/my02");
}
}

image

8.3.2 FilterRegistrationBean

  1. 声明一个配置类
  2. 注入一个FilterRegistrationBean
1
2
3
4
5
6
7
8
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
}

image

8.3.3 ServletListenerRegistrationBean

  1. 声明一个配置类
  2. 注入一个ServletListenerRegistrationBean
1
2
3
4
5
6
7
8
@Configuration(proxyBeanMethods = true)
public class MyRegistConfig {
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}

8.4 Web服务器自动配置原理以及定制化操作、

8.4.1 配置原理

8.4.2 切换Web服务器

  1. TomcatServletWebServer
  2. JettyServletWebServer
  3. UndertowServletWebServer:默认不支持jsp

切换服务器方法:

  1. 引入web场景服务器,默认Web-start自动导入tomcat,因此要先排除tomcat场景
1
2
3
4
5
6
7
依赖中排除tomcat     
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
  1. 引入其他 web场景服务器(版本均会自动仲裁)默认还是建议使用tomcat
1
2
3
4
5
6
7
8
9
10
11
tomcat场景启动器
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>

jetty场景启动器
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>

undertow场景启动器
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>

8.4.3 定制化Servlet容器

  1. 直接修改配置文件,修改server前缀即可 推荐

image

  1. 实现WebServerFactoryCustomizer

image

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
//自定义一个xxxxCustomizer自定义定制化器,实现其中的自定义方法
@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}

自定义方法注入的ConfigurableServletWebServerFactory 会自动在容器中寻寻找已经实现的serveletServer,也就是如下图所示

image

  1. 直接注入一个ConfigurableServletWebServerFactory

image

示例:最后被返回工厂即可

image

九、数据库连接与访问

9.1 导入Starter场景启动器

  1. 导入场景启动器
1
2
3
4
5
6
7
8
9
10
11
12
maven自动版本仲裁,继承maven父类
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>填写Spring版本号</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
Spring-jdbc maven启动器依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

值得注意的是,在SpringDataBase场景启动器中并未配置MySql或者其他类型的数据库驱动,因为Spring并不知道你要用哪种类型的数据库驱动,因此需要手动导入数据库驱动的依赖。

  1. 导入数据库驱动
Text
1
2
3
4
5
6

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

这里并不需要写数据库的版本号<version>5.1.49</version> 因为Spring以及提供了自动仲裁,如果需要自定义版本号,那么在maven 依赖中添加以下,覆盖Spring默认变量。

Text
1
2
3
<properties>
<mysql.version>你需要使用的版本</mysql.version>
</properties>

9.2 数据库自动配置原理

9.2.1 Spring数据库自动配置:

SpringBoot默认配置HiKari的数据源:

image

SpringAutoConfig数据库自动配置包:org.springframework.boot.autoconfigure.jdbc 包含以下重点部分:

  1. DataSourceTransactionManagerAutoConfiguration 数据库事务管理器 对应配置文件为prefix = "spring.datasource"
  2. JdbcTemplateAutoConfiguration JDBCTemolete小工具
    1. 绑定JdbcProperties 对应配置文件为 prefix = "spring.jdbc"

image

  1. 配置中导入了一个:JdbcTemplateConfiguration.class JDBCTemplete类

image

  1. JndiDataSourceAutoConfiguration JNDI Java Name Directory Interface

    JDNI实际上是一个通用资源名管理系统,不仅仅提供数据库定位,也可以给当前应用服务器所管理页面、网页、文件、连接池提供唯一标识符。名称—服务或者记录

  2. XADataSourceAutoConfiguration 分布式事务相关的

9.2.2 数据库配置案例

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver //mysql8.0以上版本之后驱动程序变化

9.3 使用Druid数据库连接池

官方文档:DruidGithub

9.3.1 配置Druid数据库连接池

9.3.1.1 手动配置基本Druid数据库连接池

  1. 配置Maven依赖:
Text
1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>

注意SpringBoot默认配置Hikari数据源,观察配置流程可以得知,当我们手动注入数据源的时候,会默认使用我们注入的,因为`@ConditionalInMIssingBean(DataSource.class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MyDataSourceConfig {

// 默认的自动配置是判断容器中没有才会配@ConditionalOnMissingBean(DataSource.class)
// @ConfigurationProperties("spring.datasource")
// @Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();

// druidDataSource.setUrl();
// druidDataSource.setUsername();
// druidDataSource.setPassword();
return druidDataSource;
}

绑定数据库配置:

  1. 可以使用Set方法向自己定义的数据源中注入属性
  2. 绑定配置文件@ConfigurationProperties("spring.datasource") 之所以绑定这个配置文件,是因为配置文件中的大部分属性名称与注入的数据源的成员变量一致,Spring会自动配置文件绑定。

9.3.1.2 使用Druid数据库连接池高级功能

  1. 配置Druid的内置监控页面官方配置方法

   根据官方文档配置spring:实际上是配置了一个Servlet处理Druid数据库连接池的Web页面响应。

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>

    因此只需要给容器中注入一个Servlet即可:*使用SpringBoot原生Web组件注册方式***ServletRegistrationBean **

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
//注册ServletBean 并且向其中添加Setvlet以及其访问路径
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(statViewServlet, "/druid/*");

registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","123456");


return registrationBean;
}

如果需要给官方Servlet注入初始化属性,那么就使用ServletRegistrationBean 提供的方法addInitParameter()来注入初始化属性。

  1. 配置Druid数据库连接池监控和统计功能

官方文档介绍到spring中,只需要给数据库连接池配置一个初始化属性filter的值即可

1
2
3
4
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
... ...
<property name="filters" value="stat" />
</bean>

Springboot中将其数据源属性中添加:

1
druidDataSource.setFilters("stat");//加入监控功能
  1. 开启Web请求的监控

官方文档中描述,需要向容器中放入一个Filter:

1
2
3
4
5
6
7
8
9
10
11
12
<filter>
<filter-name>DruidWebStatFilter</filter-name>
<filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
<init-param>
<param-name>exclusions</param-name>需要排除的静态资源
<param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DruidWebStatFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

在这里SpringBoot使用FilterRegistrationBean进行配置:

1
2
3
4
5
6
7
8
9
10
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
//注册Web原生filter
FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");

return filterRegistrationBean;
}
  1. 设置拦截路径setUrlPatterns()
  2. 设置初始化参数addInitParameter()
  3. 配置Sql注入防火墙

官方配置:只需要给数据库连接池的Filter属性增加一个值即可

  1. 只开启防火墙:
1
2
3
4
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
...
<property name="filters" value="wall"/>
</bean>
  1. 与其他功能Filter一起使用,比如用于统计监控信息的statFilter
1
2
3
4
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
...
<property name="filters" value="wall,stat"/>
</bean>

在SpringBoot中同理只需要添加一组Filter属性对应的值即可:

1
druidDataSource.setFilters("stat,wall");
  1. 开启Sping以及Session的监控

官方配置中,注入了一个Druid的拦截器

9.3.1.3 使用场景启动器配置

使用官方场景启动器自动配置maven依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>

image

可以看到自动配置中,绑定了DruidStatProperties.class, DataSourceProperties.class 这两个配置类,分别绑定了spring.datasource.druidspring.datasource 这两个配置文件前缀,因此只需要在配置文件中修改响应的属性即可。

image

  1. DruidSpringAopConfiguration.class 配置监控Spring

image

  1. DruidStatViewServletConfiguration.class 开启监控页面

image

观察配置类可以发现:默认关闭状态监控页面Servlet

image

  1. DruidWebStatFilterConfiguration.class 配置监控和统计功能

image

  1. DruidFilterConfiguration.class配置开启哪些功能,比如stat和防火墙功能

image

配置示例:

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
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver


druid:
aop-patterns: com.atguigu.admin.* #springbean监控
filters: stat,wall,slf4j #所有开启的功能

stat-view-servlet: #监控页配置
enabled: true
login-username: admin
login-password: admin
resetEnable: false

web-stat-filter: #web监控
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'


filter:
stat: #sql监控
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall: #防火墙
enabled: true
config:
drop-table-allow: false

常用功能配置列表

9.4 使用MyBatis

9.4.1 使用Starter配置

官方SpringBoot地址:GIthub-Mybatis

1
2
3
4
<dependency>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
</dependency>

9.4.2 自动配置原理

  1. 引入pom依赖之后,会增加jdbc 以及SpringBoot-Mybatis依赖文件

image

  1. 并且在SpringBoot启动之后会自动加载org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 两个配置类

image

  1. 自动配置类MybatisAutoConfiguration

image

image

自动配置部分内容:

  1. 自动配置`SqlSession---打开一个SqlSession会话的工厂类`

image

  2. 自动配置`sqlSessionTemplate----是一个SqlSession的具体实现类`

image

  3. 额外的会自动注入`@Import(AutoConfiguredMapperScannerRegistrar.class)` 实际上观察配置源码可以发现,只要我们使用Mapper注解Mapper接口就会被自动配置:

image

9.4.3 Mybatis手动配置

  1. 配置MyBatis配置文件,在全局配置yaml文件中配置mybatis
1
2
3
4
5
6
# 配置mybatis规则、使用MyBatisPlus则此项配置无效
mybatis:
config-location: classpath:mybatis/mybatis-config.xml //mybatis全局配置文件
mapper-locations: classpath:mybatis/mapper/*.xml //mapper为之
configuration: # 指定mybatis全局配置文件中的相关配置项
map-underscore-to-camel-case: true
  1. 编写Mapper接口,标注@Mapper 接口注解
  2. 编写Mapper.xml文件

或者配置yaml配置文件中的属性,就相当他于更改mybatis全局配置文件中的值。也就是在yaml中配置了,那么mybatis全局配置文件就不要写,不能同时存在!!!

image

9.4.4 Mybatis注解整合

SpringBoot初始化项目时进行引入整合SpringBoot-mybatis-starter

1
2
3
注解编写Mapper文件:
@Select("select * from city where id=#{id}")
public City getById(Long id);

需要增加mapper接口的功能,比如增加主键自增等…

1
2
增加Option注解标签
@Options(useGeneratedKeys = true,keyProperty = "id")

9.4.4 最佳使用方法

  1. 引入mybatis-starter
  2. 配置application.xml文件内中的mybatis配置规则
  3. 在**application.xml 指定Mapper.xml所在位置**
  4. 编写Mapper接口,并且使用@Mapper** 注解进行标注,或者在SpringBoot某一个配置类上标注****@mapperScan**** 指定包扫描Mapper**
  5. 简单方法使用@Select @Delete @Insert @Update** 等注解直接编写,使用****@Option**** 注解增加当前Mapper功能**
  6. 复杂文件在mapper.xml中进行编写即可。

9.5 使用MyBatis Plus

官方文档:Mybatis-Plus

9.5.1 配置MyBatisPlus

引入官方Stater配置依赖:

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>

9.5.2 自动配置原理

  1. 引入Starter后,会自动导入如下依赖,也就是说,使用MybatisPlus之后不需要再重复引入**mybatis jdbc 的相关启动器**

image

  1. 自动配置类会在Springboot加载之后,自动再容器中注入几个配置类:
1
2
3
4
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.autoconfigure.IdentifierGeneratorAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration
  1. 自动配置类:MybatisPlusAutoConfiguration
    1. 自动配置绑定的配置类@EnableConfigurationProperties(MybatisPlusProperties.class)

image

  1. 绑定配置文件前缀:mybatis-plus

image

  1. 自动注入SqlSessionFactory

image

  1. 实际上也自动配置了Mapper-loction的位置:任意包的类路径下的所有Mapper文件夹下的任意路径都是mapper.xml的sql映射文件路径。

image

  1. 也会自动配置SqlSessionFactory

image

  1. 自动注册Mapper注解标注的mapper文件

image

9.5.3 使用MBP开发

9.5.4 基础开发

  1. 引入数据源配置
  2. 引入MBP启动器
  3. 开启MapperScan或者Mapper注解
  4. 编写实体类
  5. 编写对应的Mapper文件(继承基类BaseMapper 你需要操作的类)

BaseMapper提供了一些基本的增删改查的方法,****并且可以使用@TableField(exist=false) 注解标注表示当前属性在表中不存在。

注意:

  1. 实体类名称与表名不一致

image

9.5.4 mybatisPlus简化开发Service

IService 规定一系列的增删改查的业务方法,简化Service编写

image

IService顶级接口,存在一个实现类 ServiceImpl<M extends BaseMapper, T>实现了顶级方法的增删改查方法

image

可以支持常见的查询集合,分页数据等….

使用方法:

  • 使用自定义Service接口继承顶级接口IService
  • 自定义的ServiceImp继承顶级接口的实现类ServiceImpl,并且实现自定义Service接口方法

分页开发:****官方地址

需要配置分页插件才可以使用:在配置文件中注入MybatisPlusInterceptor,这个拦截器中可以执行默认的拦截器,另外存在一个**public void addInnerInterceptor(InnerInterceptor innerInterceptor) 方法,添加了一个***InnerInterceptor 打开实现可知:一个分页的拦截器PaginationInnerInterceptor* 。

image

因此在使用分页插件的时候,只需要注入通用的MybatisPlusInterceptor **** 拦截器,并且调用addInnerInterceptor(InnerInterceptor innerInterceptor)** 向其中添加分页拦截器PaginationInnerInterceptor即可**。

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
@Configuration
@MapperScan("scan.your.mapper.package")
public class MybatisPlusConfig {

/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
return interceptor;
}

@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.setUseDeprecatedExecutor(false);
}
}

    调用顶级接口IService接口中被ServiceImpl实现的方法:default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper传入两个参数:

  1. page:分页对象——封装分页参数信息

image

  2. `queryWrapp` :条件封装对象

示例:

1
2
3
4
//构造分页参数  分页第几页,每一页的页面大小
Page<User> page = new Page<>(pn, 2);
//调用page进行分页,返回的page对象包括一系列分页信息,比如多少页,当前第几页,含有多少记录
Page<User> userPage = userService.page(page, null);

Themeleaf模板引擎也支持,路径变量参数传递等功能,参照官方文档

十、单元测试Junit5

SpringBoot整合Junit以后。

  • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
  • Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚

10.1 配置Junit5

SpringBoot引入Junit5测试框架:

1
2
3
4
5
6
引入junit5官方启动器pom依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  1. JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
  2. JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部 包含了一个测试引擎,用于在Junit Platform上运行。
  3. JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎。

image

SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖,默认不支持Junit4,3

10.2 支持Junit4

1
2
3
4
5
6
7
8
9
10
11
12
引入POM依赖:
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>

10.3 Junit5注解

JUnit5的注解与JUnit4的注解有所变化

https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

  • **@Test :**表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • **@ParameterizedTest :**表示方法是参数化测试,下方会有详细介绍
  • **@RepeatedTest :**表示方法可重复执行,下方会有详细介绍
  • **@DisplayName :**为测试类或者测试方法设置展示名称
  • **@BeforeEach :**表示在每个单元测试之前执行
  • **@AfterEach :**表示在每个单元测试之后执行
  • **@BeforeAll :**表示在所有单元测试之前执行
  • **@AfterAll :**表示在所有单元测试之后执行
  • **@Tag :**表示单元测试类别,类似于JUnit4中的@Categories
  • **@Disabled :**表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
  • **@Timeout :**表示测试方法运行如果超过了指定时间将会返回错误
  • **@ExtendWith :**为测试类或测试方法提供扩展类引用

10.4 断言Assertions

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:

检查业务逻辑返回的数据是否合理。

所有的测试运行结束以后,会有一个详细的测试报告:

10.4.1 简单断言

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null

10.4.2 数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等

1
2
3
4
5
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

10.4.3 组合断言

assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言

1
2
3
4
5
6
7
8
@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}

10.4.4 异常断言

JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用

1
2
3
4
5
6
7
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}

10.4.5 超时断言

Assertions.assertTimeout()为测试方法设置了超时时间

1
2
3
4
5
6
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}

10.4.6 快速失败

fail方法后的代码不进行执行

1
2
3
4
5
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}

10.5 假设Assumptions

(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";

@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}

@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}

10.6 嵌套测试

使用内部类和@Nested注解实现嵌套测试,内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。外部测试方法无法驱动内部的BeforeEach以及AfterEach,但是内部可以驱动外部!!

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
70
71
@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}

@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}

@Nested
@DisplayName("after pushing an element")
class AfterPushing {

String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}

10.7 参数化测试

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

  1. @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  2. @NullSource: 表示为参数化测试提供一个null的入参
  3. @EnumSource: 表示为参数化测试提供一个枚举入参
  4. @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
  5. @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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");
}

十一、指标监控

SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
https://github.com/codecentric/spring-boot-admin** spring可视化管理页面**

11.1 使用方法

  1. 引入SpringBoot Actuator启动器
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 配置文件中进行配置
1
2
3
4
5
6
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露
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 执行线程转储。

11.2 Health Endpoint

健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

Text
1
2
开启health的显示详细
endpoint.health.show-details=always

重要的几点:

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告有任何一个应用是宕机状态,整个就是宕机状态
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
  • 可以很容易的添加自定义的健康检查机制

11.3 Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;

  • 通过Metrics对接多种监控系统
  • 简化核心Metrics开发
  • 添加自定义Metrics或者扩展已有Metrics

11.4 管理Endpoints

11.4.1 开启与禁用Endpoints

  • 默认所有的Endpoint除过shutdown都是开启的。
  • 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint..enabled = true
1
2
3
4
5
6
7
8
9
10
11
12
13
management:
endpoint:
beans:
enabled: true
或者禁用所有的Endpoint然后手动开启指定的Endpoint
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true

11.4.2 暴露Endpoints

支持的暴露方式

  • HTTP:默认只暴露healthinfo 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

11.4.3 定制化EndPoints

11.4.3.1 定制化Health信息

  1. 继承抽象类AbstractHealthIndicator 实现其中的doHealthCheck() 方法
  2. 使用@Component注解注入容器
  3. 开启配置文件中,显示详情信息
1
2
3
4
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息

使用示例:

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
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {

/**
* 真实的检查方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb。 获取连接进行测试
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 2){
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}


builder.withDetail("code",100)
.withDetails(map);

}
}

10.4.3.2 定制化Info信息

10.4.3.2.1 使用配置文件
1
2
3
4
5
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@
10.4.3.2.2 使用InfoContributer

访问:http://localhost:8080/actuator/info 返回详细信息

  1. 实现InfoContributer接口
  2. 使用@Component注入容器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Collections;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;

@Component
public class ExampleInfoContributor implements InfoContributor {

@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}

}
10.4.3.2.3 定制Metrics

SpringBoot支持自动适配的Metrics

  • JVM metrics, report utilization of:
  • Various memory and buffer pools
  • Statistics related to garbage collection
  • Threads utilization
  • Number of classes loaded/unloaded
  • CPU metrics
  • File descriptor metrics
  • Kafka consumer and producer metrics
  • Log4j2 metrics: record the number of events logged to Log4j2 at each level
  • Logback metrics: record the number of events logged to Logback at each level
  • Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
  • Tomcat metrics (server.tomcat.mbeanregistry.enabled must be set to true for all Tomcat metrics to be registered)
  • Spring Integration metrics

通过在业务逻辑方法构造函数中传入**MeterRegistry 类也就是Metrics的注册中心,在构造方法中调用注册中心,进行注册一个新的指标**

1
2
3
4
5
6
7
8
9
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}
public void hello() {
counter.increment();
}
}

10.4.3.3 自定义监控端点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Endpoint(id = "container")//使用这个注解进行注释即可,表示当前新建的端点名称
public class DockerEndpoint {


@ReadOperation//表示这个是一个读操作,用户获取监控信息
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}

@WriteOperation//这是一个写操作,以后用于线上监控
private void restartDocker(){
System.out.println("docker restarted....");
}

}

十二、配置文件解析

12.1 Profile功能

为了方便多环境适配,springboot简化了profile功能。

12.2 application-profile功能

  • 默认配置文件 application.yaml;任何时候都会加载
  • 指定环境配置文件 application-{env}.yaml
  • 激活指定环境
    • 配置文件激活 spring.profiles.active
    • 命令行激活:java -jar xxx.jar –spring.profiles.active=prod –person.name=haha
  • 修改配置文件的任意值,命令行优先
  • 默认配置与环境配置同时生效
  • 同名配置项,profile配置优先

12.3 @Profile条件装配功能

Text
1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@Profile("production")
public class ProductionConfiguration {

// ...

}

12.4 profile分组

Text
1
2
3
4
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq

使用:--spring.profiles.active=production 激活

12.5 配置文件应用方法

常用:Java属性文件、YAML文件、环境变量、命令行参数;

12.5.1 配置文件查找位置

(1) classpath 根路径

(2) classpath 根路径下config目录

(3) jar包当前目录

(4) jar包当前目录的config目录

(5) /config子目录的直接子目录

12.5.2 配置文件加载顺序:

  1.  当前jar包内部的application.properties和application.yml
  2.  当前jar包内部的application-{profile}.properties 和 application-{profile}.yml
  3.  引用的外部jar包的application.properties和application.yml
  4.  引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml

12.5.3 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项

十三、自定义Starter

  1. 新建 一个空的Maven项目
  2. 新建一个启动器模块—->
  3. 新建一个自动配置模块—->
    • Autoconfigure包中配置使用 META-INF/spring.factoriesEnableAutoConfiguration 的值,使得项目启动加载指定的自动配置类
    • 编写自动配置类 xxxAutoConfiguration -> xxxxProperties
    • @Configuration
    • @Conditional
    • @EnableConfigurationProperties
    • @Bean
    • ……
  4. 启动器模块中pom引入自动配置模块