SpringBoot2知识学习及查漏补缺

基本介绍

优缺点

优点:

  • 创建独立Spring应用

  • 内嵌web服务器

  • 自动starter依赖,简化构建配置

  • 自动配置Spring以及第三方功能

  • 提供生产级别的监控、健康检查及外部化配置

  • 无代码生成、无需编写XML

缺点:

  • 人称版本帝,迭代快,需要时刻关注变化
  • 封装太深,内部原理复杂,不容易精通

时代背景

微服务
  • 微服务是一种架构风格
  • 一个应用拆分为一组小型服务
  • 每个服务运行在自己的进程内,也就是可独立部署和升级
  • 服务之间使用轻量级HTTP交互
  • 服务围绕业务功能拆分
  • 可以由全自动部署机制独立部署
  • 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
分布式

分布式的困难:

  • 远程调用
  • 服务发现
  • 负载均衡
  • 服务容错
  • 配置管理
  • 服务监控
  • 链路追踪
  • 日志管理
  • 任务调度

分布式的解决方案:

  • SpringBoot + SpringCloud
云原生

上云的困难:

  • 服务自愈
  • 弹性伸缩
  • 服务隔离
  • 自动化部署
  • 灰度发布
  • 流量治理

上云的解决方案:

  • docker容器化技术
  • k8s容器编排
  • DevOps,CI/CD
  • Service Mesh于Serviceless

相关文档

查看版本新特性:

Home · spring-projects/spring-boot Wiki (github.com)

官方文档:

Spring Boot Reference Documentation

自动配置原理

注解说明

@Configuration
  • 加在类上,告诉SpringBoot这是一个配置类

  • 配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的

  • 配置类本身也是组件

  • proxyBeanMethods属性:标识配置类是否是代理@Bean方法

    • Full模式(proxyBeanMethods = true):保证每个@Bean方法被调用多少次返回的组件都是单实例的
    • Lite模式(proxyBeanMethods = false):每个@Bean方法被调用多少次返回的组件都是新创建的
    • 组件依赖必须使用Full模式,其他情况可以使用Lite模式(加快项目启动)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class DemoConfig {
/**
* 给容器中添加组件
* 以方法名作为组件的id;返回类型就是组件类型;返回的值,就是组件在容器中的实例
*/
@Bean
public Demo demo1() {
Demo demo = new Demo(1, "demo");
// 依赖SubDemo组件
demo.setSubDemo(subDemo());
return demo;
}
@Bean
public SubDemo subDemo() {
return new SubDemo();
}
}
1
2
3
4
5
6
7
8
9
private static void testConfigurationProxyBeanMethods(ConfigurableApplicationContext ctx) {
// 测试@Confirguaion(proxyBeanMethods = true/ false)
// DemoConfig 配置类本身也是组件
DemoConfig demoConfig = ctx.getBean(DemoConfig.class);
Demo demo1 = demoConfig.demo1();
Demo demo2 = demoConfig.demo1();
log.info("{}", demo1);
log.info("{}", demo1 == demo2); // proxyBeanMethods = true, output is true
}
@Bean、@Component、@Controller、@Service、@Repository

标识类是一个组件,被spring管理

@ComponentScan

组件扫描

@Import

在已标记为组件的类上使用

自动给导入的类型注册为组件,默认组件的名称为全类名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Import(Demo.class)
@Configuration
public class DemoConfig {
/**
* 给容器中添加组件
* 以方法名作为组件的id;返回类型就是组件类型;返回的值,就是组件在容器中的实例
*/
@Bean
public Demo demo1() {
Demo demo = new Demo(1, "demo");
// 依赖SubDemo组件
demo.setSubDemo(subDemo());
return demo;
}
@Bean
public SubDemo subDemo() {
return new SubDemo();
}
}
1
2
3
4
5
6
private static void testImportAnnotation(ConfigurableApplicationContext ctx) {
String[] names = ctx.getBeanNamesForType(Demo.class);
Arrays.stream(names).forEach(System.out::println);
// com.zhuweitung.springboot2.model.Demo 此为@Import方式注册
// demo1 此为使用@Bean方法注册
}
@Conditional

按照条件装配,当满足指定的条件时才将组件注入

  • @ConditionalOnBean:当容器中存在某些组件
  • @ConditionalOnMissingBean:当容器中不存在某些组件
  • @ConditionalOnClass:当容器中有某些类(有依赖jar包)
  • @ConditionalOnMissingClass:当容器中没有某些类
  • @ConditionalOnResource:当项目路径下存在某些资源
  • @ConditionalOnJava:当是指定的Java版本号
  • @ConditionalOnWebApplication:当工程是web应用
  • @ConditionalOnNotWebApplication:当工程不是web应用
  • @ConditionalOnProperty:当配置文件中配置了某些属性
@ImportResource

导入spring的xml配置文件

1
2
@ImportResource("classpath:spring/beans.xml")
public class MyConfig {}

配置绑定

@ConfigurationProperties

添加在类上,prefix属性指定在配置文件中的前缀

1
2
3
4
5
demo:
config:
url: 'http://who.where.com'
username: 'root'
pwd: '123456'
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
@ConfigurationProperties(prefix = "demo.config")
public class DemoProperties {
private String url;
private String username;
private String pwd;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return new StringJoiner(", ", DemoProperties.class.getSimpleName() + "[", "]")
.add("url='" + url + "'")
.add("username='" + username + "'")
.add("pwd='" + pwd + "'")
.toString();
}
}

只有注册到容器中才能自动加载

注册方式有两种:

  • 类上加上@Component注解

    1
    2
    3
    @Component
    @ConfigurationProperties(prefix = "demo.config")
    public class DemoProperties {
  • 在其他组件上加上@EnableConfigurationProperties注解, 并指定注册的类

    1
    2
    3
    @Configuration
    @EnableConfigurationProperties(DemoProperties.class)
    public class DemoConfig {}

引导加载自动配置类

程序启动类上加的@SpringBootApplication注解,是由三个注解构成

1
2
3
4
5
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}
@SpringBootConfiguration
1
2
3
@Configuration
@Indexed
public @interface SpringBootConfiguration {}

由以上代码可以看出,加上@SpringBootConfiguration注解后会把类标记为配置类

@ComponentScan

组件扫描

@EnableAutoConfiguration

开启自动配置

1
2
3
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
  • @AutoConfigurationPackage:

    1
    2
    3
    // 给容器中导入一个组件,通过Registrar将启动类所在包下的所有组件注册到容器
    @Import(AutoConfigurationPackages.Registrar.class)
    public @interface AutoConfigurationPackage {}
  • @Import(AutoConfigurationImportSelector.class)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 获取所有需要导入的组件
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
    }
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = getConfigurationClassFilter().filter(configurations);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
    }
    • 利用 getAutoConfigurationEntry 给容器中批量导入一些组件

    • 调用 getCandidateConfigurations 获取到所有需要导入到容器中的配置类

    • 利用工厂加载 loadSpringFactories 得到所有的组件

    • META-INF/spring.factories位置来加载一个文件

      • 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件
      • spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

      • springboot2.7以上不推荐将自动配置类写到 META-INF/spring.factories,而是推荐写到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

      • 总之就是在这些文件中已经写死了springboot一启动就要给容器中加载的所有配置类

按需开启自动配置项

按照上面一步操作后所有自动配置类启动时默认全部加载,但是按照条件装配规则(@Conditional),最终会按需配置

总结

  • SpringBoot先加载所有的自动配置类 xxxAutoConfiguration

  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxProperties里面拿。xxxProperties和配置文件进行了绑定

  • 生效的配置类就会给容器中装配很多组件

  • 只要容器中有这些组件,相当于这些功能就有了

  • 定制化配置

    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改

最佳实践

YAML

基本语法

  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • '#'表示注释
  • 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义

自定义配置类提示

pom中引入依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

增加构建插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

Web开发

参考官方文档

SpringMVC自动配置

Spring Boot为Spring MVC提供了自动配置功能,对大多数应用程序都很适用。

自动配置在Spring的默认值基础上增加了以下功能。

  • 包含了 ContentNegotiatingViewResolverBeanNameViewResolver Bean
  • 支持为静态资源提供服务,包括对WebJars的支持
  • 自动注册 ConverterGenericConverterFormatter Bean
  • 支持 HttpMessageConverters
  • 自动注册 MessageCodesResolver
  • 支持静态的 index.html
  • 自动使用 ConfigurableWebBindingInitializer bean

如果你想保留那些Spring Boot MVC定制,并进行更多的 MVC定制(Interceptor、Formatter、视图控制器和其他功能),你可以添加你自己的 @Configuration 类,类型为 WebMvcConfigurer ,但 @EnableWebMvc

如果你想提供 RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver 的自定义实例,并仍然保持Spring Boot MVC的自定义,你可以声明一个 WebMvcRegistrations 类型的bean,用它来提供这些组件的自定义实例。

如果你想完全控制Spring MVC,你可以添加你自己的 @Configuration 并使用 @EnableWebMvc 注解 ,或者添加你自己的 @Configuration 并使用 DelegatingWebMvcConfiguration 注解 ,如 @EnableWebMvc 的Javadoc中所述。

HttpMessageConverter

Spring MVC使用 HttpMessageConverter 接口来转换HTTP请求和响应

如果需要添加或定制转换器,你可以使用Spring Boot的 HttpMessageConverters

1
2
3
4
5
6
7
8
9
10
11
@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {

@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new AdditionalHttpMessageConverter();
HttpMessageConverter<?> another = new AnotherHttpMessageConverter();
return new HttpMessageConverters(additional, another);
}

}

静态内容

默认情况下,Spring Boot从classpath中的 /static(或 /public/resources/META-INF/resources)目录或 ServletContext 的root中提供静态内容。

资源访问路径:项目访问根路径/静态资源名

它使用了Spring MVC中的 ResourceHttpRequestHandler,因此你可以通过添加你自己的 WebMvcConfigurer 和覆盖 addResourceHandlers 方法来修改该行为。

改变默认的静态资源路径

即访问路径前缀,默认是/**

1
2
3
spring:
mvc:
static-path-pattern: /resources/**

做以上配置后的资源访问路径为:项目访问根路径/resources/静态资源名

自定义静态资源文件存放位置
1
2
3
4
5
spring:
web:
resources:
static-locations: # 值为数组格式
- classpath:/haha/

配置了自定义的存放位置后,默认的4个路径就会失效

欢迎页面

Spring Boot同时支持静态和模板化的欢迎页面。 它首先在配置的静态内容位置寻找一个 index.html 文件。 如果没有找到,它就会寻找 index 模板。 如果找到了其中之一,它就会自动作为应用程序的欢迎页面使用。

访问 项目访问根路径/或项目访问根路径/index.html 会解析到欢迎页面

若配置了自定义的静态资源路径则失效

静态内容加载原理
  • Springboot启动后加载各依赖组件的自动配置类

  • SpringMVC功能的自动配置类为 WebMvcAutoConfiguration

    1
    2
    3
    4
    5
    6
    7
    @AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
    ValidationAutoConfiguration.class })
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    public class WebMvcAutoConfiguration {}
  • 内部有一个WebMvcAutoConfigurationAdapter配置类

    1
    2
    3
    4
    5
    @Configuration(proxyBeanMethods = false)
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {}
  • 将配置文件与WebMvcPropertiesWebProperties类进行绑定

  • 执行addResourceHandlers方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // add-mappings=false时禁用静态资源访问
    if (!this.resourceProperties.isAddMappings()) {
    logger.debug("Default resource handling disabled");
    return;
    }
    // 将访问 /webjars/** 的请求加入到资源处理器
    addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
    // 将访问 static-path-pattern 配置的请求加入到资源处理器,并指定资源路径为 static-locations 配置的值
    addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
    registration.addResourceLocations(this.resourceProperties.getStaticLocations());
    if (this.servletContext != null) {
    ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
    registration.addResourceLocations(resource);
    }
    });
    }

请求参数处理

HiddenHttpMethodFilter

通过表单隐藏域,以post方式提交一个_method字段给服务器,通过HiddenHttpMethodFilter处理包装后,请求的method变为了传入的合法方式,以此实现RESTful风格的接口

WebMvcAutoConfiguration类中有以下代码

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

通过上面代码可以知道,要开启还需要进行以下配置

1
2
3
4
5
spring:
mvc:
hiddenmethod:
filter:
enabled: true
请求映射原理

SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet->doDispatch()开始

doDispatch调用getHandler方法获取处理器

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

RequestMappingHandlerMapping保存了所有@RequestMapping和handler的映射规则

所有的请求映射都在HandlerMapping中

  • SpringBoot自动配置了 WelcomePageHandlerMapping、RequestMappingHandlerMapping
  • 请求进来后会遍历所有的HandlerMapping,来查找映射的handler
  • 当需要自定义映射处理时,可以自己自定义HandlerMapping,并注册到容器中

参数注解

常见的有@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody

@MatrixVariable

@MatrixVariable即矩阵变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1、请求路径:/person/lisi;age=30,hobby=a,b,c/wangmei;age=29,hobby=e,f
//2、SpringBoot默认是禁用了矩阵变量的功能,需要手动开启
// 原理:对于路径的处理通过UrlPathHelper进行解析,它的removeSemicolonContent属性表示会移除分号后的内,开启矩阵变量功能需要实现WebMvcConfigurer接口并重写configurePathMatch,并将UrlPathHelper的removeSemicolonContent设置为false
//3、矩阵变量必须有url路径变量才能被解析

@GetMapping("/person/{father}/{mother}")
public String family(@PathVariable("father") String father,
@MatrixVariable(pathVar = "father", value = "age") Integer fatherAge,
@MatrixVariable(pathVar = "father", value = "hobby") List<String> fatherHobby,
@PathVariable("mother") String mother,
@MatrixVariable(pathVar = "mother", value = "age") Integer motherAge,
@MatrixVariable(pathVar = "mother", value = "hobby") List<String> motherHobby) {
return "family";
}

参数处理原理

  • doDispatch方法中先充HandlerMapping中找到处理请求的handler(Controller.methd())

  • 找到当前handler的适配器HandlerAdapter(RequestMappingHandlerAdapter)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    if (this.handlerAdapters != null) {
    for (HandlerAdapter adapter : this.handlerAdapters) {
    if (adapter.supports(handler)) {
    return adapter;
    }
    }
    }
    throw new ServletException("No adapter for handler [" + handler +
    "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
    }

    • RequestMappingHandlerAdapter:支持方法上标注@RequestMapping
    • HandlerFunctionAdapter:支持函数式编程的
  • 执行适配器的handle方法,调用RequestMappingHandlerAdapterhandleInternal方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    protected ModelAndView handleInternal(HttpServletRequest request,
    HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    ModelAndView mav;
    checkRequest(request);
    // ...
    mav = invokeHandlerMethod(request, response, handlerMethod);
    // ...
    return mav;
    }
  • 执行invokeHandlerMethod方法,方法内会设置参数解析器

    1
    2
    3
    if (this.argumentResolvers != null) {
    invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    }

​ 由上面的参数解析器可以知道,SpringMVC目标方法能写多少种参数类型,取决于参数解析器

  • 调用invokeAndHandle->invokeForRequest->getMethodArgumentValues方法,解析参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
    Object... providedArgs) throws Exception {
    MethodParameter[] parameters = getMethodParameters();
    if (ObjectUtils.isEmpty(parameters)) {
    return EMPTY_ARGS;
    }
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
    MethodParameter parameter = parameters[i];
    parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
    args[i] = findProvidedArgument(parameter, providedArgs);
    if (args[i] != null) {
    continue;
    }
    // 判断是否支持解析当前参数,内部会循环判断解析器是否支持
    if (!this.resolvers.supportsParameter(parameter)) {
    throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
    }
    try {
    // 执行参数解析
    args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
    }
    catch (Exception ex) {
    // Leave stack trace for later, exception may actually be resolved and handled...
    if (logger.isDebugEnabled()) {
    String exMsg = ex.getMessage();
    if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
    logger.debug(formatArgumentError(parameter, exMsg));
    }
    }
    throw ex;
    }
    }
    return args;
    }
  • resolveArgument方法中会创建WebDataBinder对象,该对象中的GenericConversionService属性包含大量类型转换器,用于将字符串转换为对应的类型

  • 将所有的数据都放在ModelAndViewContainer,包含要去的页面地址,以及Model(BingdingAwareModelMap)数据

  • 执行processDispatchResult->render->renderMergedOutputModel

自定义类型转换器

前端提交数据格式

后端接收格式

1
2
3
4
@PostMapping("create")
public String create(Demo demo) {
return "success";
}

类型转换器

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
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) {
// 添加自定义类型转换器
registry.addConverter(new Converter<String, Demo>() {
@Override
public Demo convert(String source) {
if (source != null) {
Demo demo = new Demo();
String[] parts = source.split("@");
if (parts.length > 0) {
try {
demo.setId(Integer.parseInt(parts[0]));
} catch (NumberFormatException e) {}
}
if (parts.length > 1) {
demo.setName(parts[1]);
}
return demo;
}
return null;
}
});
}
};
}
}

数据响应与内容协商

数据响应流程
1
2
3
4
@GetMapping("demo")
public Demo demo() {
return new Demo(666, "老铁");
}

请求上面接口后,返回值处理过程如下

  • 执行invokeForRequest方法后获取到返回值,遍历返回值解析器对返回值进行处理

    1
    2
    3
    4
    try {
    this.returnValueHandlers.handleReturnValue(
    returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    }

    • SpringMVC支持以下类型的返回值
      • ModelAndView
      • Model
      • View
      • ResponseBodyEmitter
      • StreamingResponseBody
      • HttpEntity
      • HttpHeaders
      • Callable
      • DeferredResult
      • AsyncTask
      • 方法有@ModelAttribute注解
      • @ResponseBody注解
      • void或字符串
      • Map
      • 在默认解析模式下,对于任何不是简单类型的返回值类型
  • 遍历找到支持返回值类型的解析器后,执行解析器的handleReturnValue方法

  • RequestResponseBodyMethodProcessor处理器可以处理加了@ResponseBody注解的

  • 执行writeWithMessageConverters方式,会利用消息转换器(MessageConverters)来处理

  • 若返回数据不是流类型(InputStreamResource、Resource)的,会进行内容协商(浏览器在请求头中告诉服务器它可以接受什么类型的内容)

  • 内容协商完成获取到mediaType

  • 遍历所有消息转换器,判断是否支持(supports)返回值类型,是否可以写为(canWrite)mediaType类型的内容

    • byte:[application/octet-stream, */*]
    • String:[text/plain, */*]
    • String:[text/plain, */*]
    • Resource:[*/*]
    • ResourceRegion:[application/xml, text/xml, application/*+xml]
    • DOMSource、SAXSource、StAXSource、StreamSource、Source
    • MultiValueMap:[application/x-www-form-urlencoded, multipart/form-data, multipart/mixed]
    • 任意类型:[application/json, application/*+json]
    • 任意类型:[application/json, application/*+json]
    • 支持注解方式xml处理
  • 最终 MappingJackson2HttpMessageConverter 把对象转为JSON写到响应体中

内容协商原理

客户端通过请求头中不同的Accept值来告诉服务器本客户端可以接收的数据类型

内容协商的代码在AbstractMessageConverterMethodProcessorwriteWithMessageConverters方法中

  • 先从响应头中获取媒体类型,若没有则继续协商

    1
    2
    MediaType selectedMediaType = null;
    MediaType contentType = outputMessage.getHeaders().getContentType();
  • 从请求头中获取客户端支持的类型

    1
    acceptableTypes = getAcceptableMediaTypes(request);
  • 遍历内容协商策略,默认使用基于请求头的策略,返回请求头中描述的支持的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
    for (ContentNegotiationStrategy strategy : this.strategies) {
    List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
    if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
    continue;
    }
    return mediaTypes;
    }
    return MEDIA_TYPE_ALL_LIST;
    }

  • 获取服务端能生成的媒体类型

    1
    List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
  • 获取服务端能生成且客户端支持的媒体类型

    1
    2
    3
    4
    5
    6
    7
    8
    List<MediaType> mediaTypesToUse = new ArrayList<>();
    for (MediaType requestedType : acceptableTypes) {
    for (MediaType producibleType : producibleTypes) {
    if (requestedType.isCompatibleWith(producibleType)) {
    mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
    }
    }
    }
  • 按媒体类型权重值进行排序,并以第一个媒体类型作为最终使用的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    MediaType.sortBySpecificityAndQuality(mediaTypesToUse);

    for (MediaType mediaType : mediaTypesToUse) {
    if (mediaType.isConcrete()) {
    selectedMediaType = mediaType;
    break;
    }
    else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
    selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
    break;
    }
    }
  • 遍历消息解析器,获取支持生成对应媒体类型的解析器

    1
    2
    3
    for (HttpMessageConverter<?> converter : this.messageConverters) {
    // ...
    }
  • 调用消息解析器的write方法将数据写到响应中

开启基于请求参数方式内容协商功能

修改配置文件:

1
2
3
spring:
contentnegotiation:
favor-parameter: true # 开启请求参数内容协商模式

请求格式:

1
http://localhost:8123/test/demo?format=xml
自定义消息转换器

实现接口HttpMessageConverter

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
public class CustomMessageConverter implements HttpMessageConverter<Demo> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// 不进行读取解析,直接返回false
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// 对转换的类型做限制
return clazz.isAssignableFrom(Demo.class);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
// 自定义媒体类型
return MediaType.parseMediaTypes("application/z-custom");
}
@Override
public Demo read(Class<? extends Demo> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Demo demo, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// 将数据以需要的形式写入到响应中
outputMessage.getBody().write(demo.toString().getBytes());
}
}

注册自定义的消息转换器

1
2
3
4
5
6
7
8
9
10
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
// 添加自定义消息转换器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CustomMessageConverter());
}
};
}

添加自定义类型到基于请求参数方式内容协商策略中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
// 自定义内容协商策略
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json", MediaType.APPLICATION_JSON);
mediaTypes.put("xml", MediaType.APPLICATION_XML);
mediaTypes.put("z-custom", MediaType.parseMediaType("application/z-custom"));
configurer.strategies(Arrays.asList(new ParameterContentNegotiationStrategy(mediaTypes),
new HeaderContentNegotiationStrategy()));
}
};
}

添加自定义的功能有可能会覆盖默认的很多功能,导致一些默认功能失效,需要谨慎使用

视图解析原理

  • 入口还是DispatcherServlet的doDispatch方法
  • HandlerAdapter通过反射执行完控制器方法后都会返回ModelAndView对象,该对象包含数据和视图地址
  • 执行processDispatchResult方法,决定页面如何响应
  • 调用render(mv, request, response)方法进行页面渲染逻辑
  • 通过resolveViewName方法获取到View对象
    • ContentNegotiationViewResolver中包含了所有支持的视图解析器,通过遍历视图解析器获取支持的解析器,然后解析得到视图对象
  • 调用view.render(mv.getModelInternal(), request, response)进行页面渲染工作

拦截器

实现HandlerInterceptor接口

1
2
3
4
5
6
7
public class AdminInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return "1".equals(request.getParameter("force"));
}
// 根据需要实现postHandle、afterCompletion方法
}

注册拦截器

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
// 自定义拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AdminInterceptor())
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/hello");
}
};
}
拦截器原理
  • doDispatch方法中调用getHandler获取到HandlerExecutionChain对象,该对象包含了处理器和一个拦截器列表

  • 按顺序执行所有拦截器的preHandle方法

  • 若所有拦截器都放行,则执行目标方法

    • 多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关

    • preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行

      1
      2
      3
      4
      5
      6
      com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor preHandle
      com.zhuweitung.springboot2.interceptor.AdminHandlerInterceptor preHandle
      com.zhuweitung.springboot2.interceptor.AdminHandlerInterceptor postHandle
      com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor postHandle
      com.zhuweitung.springboot2.interceptor.AdminHandlerInterceptor afterCompletion
      com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor afterCompletion
  • 若某个拦截器执行preHandle没有放行

    • preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行

      1
      2
      3
      com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor preHandle
      com.zhuweitung.springboot2.interceptor.AdminHandlerInterceptor preHandle
      com.zhuweitung.springboot2.interceptor.GlobalHandlerInterceptor afterCompletion

文件上传

1
2
3
4
5
6
7
8
9
10
@PostMapping("upload")
public String upload(@RequestPart MultipartFile[] files) throws IOException {
if (files.length > 0) {
for (MultipartFile file : files) {
String newFileName = UUID.randomUUID().toString() + "." + FileUtil.getSuffix(file.getOriginalFilename());
file.transferTo(new File("D:\\tmp\\upload\\" + newFileName));
}
}
return "success";
}

配置文件上传大小

1
2
3
4
5
spring:
servlet:
multipart:
max-file-size: 50MB # 单个文件最大值
max-request-size: 100MB # 单次请求文件大小总量最大值
自动配置原理
  • 自动配置类为MultipartAutoConfiguration,属性类为MultipartProperties

  • MultipartAutoConfiguration中将StandardServletMultipartResolver注册为multipartResolver(文件上传解析器)

  • 文件上传请求会进入DispatcherServlet的doDispatch方法

  • 进入checkMultipart方法,使用配置的文件上传解析器判断参数是否是multipart(判断 ContentType 上是否以 multipart 开始)

    1
    2
    3
    4
    5
    @Override
    public boolean isMultipart(HttpServletRequest request) {
    return StringUtils.startsWithIgnoreCase(request.getContentType(),
    (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
    }
  • 使用参数解析器RequestPartMethodArgumentResolver对参数进行解析

  • 将request中文件信息封装为一个Map,MultiValueMap<String, MultipartFile>

  • 解析完成后将MultipartFile数组交给控制器方法处理

异常处理

默认规则
  • 默认情况下,Spring Boot提供/error处理所有错误的映射
  • 对于浏览器,响应whitelabel错误视图,以html形式呈现错误信息
  • 对于非浏览器客户端,响应json数据其中包含错误,HTTP状态和异常消息的详细信息
自动配置原理
  • ErrorMvcAutoConfiguration是异常处理规则的自动配置类

  • 注册了DefaultErrorAttributes(errorAttributes)组件,定义错误信息中包含哪些内容

    1
    2
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {}
  • 注册了BasicErrorController组件,默认路径为/error,有两个处理方法,分别返回页面(new ModelAndView(“error”, model), error为下面注册的错误视图)和Json

    • 根据请求头中Accept是否为text/html,进入不同方法
    1
    2
    3
    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class BasicErrorController extends AbstractErrorController {}
  • 注册了一个id为error的错误视图

  • 注册了BeanNameViewResolver视图解析器,作用是根据bean名称解析,服务于上面的错误视图

  • 注册了DefaultErrorViewResolver,将不同错误渲染到不同页面上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static {
    Map<Series, String> views = new EnumMap<>(Series.class);
    views.put(Series.CLIENT_ERROR, "4xx");
    views.put(Series.SERVER_ERROR, "5xx");
    SERIES_VIEWS = Collections.unmodifiableMap(views);
    }

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    }
    return modelAndView;
    }
异常处理流程
  • 执行目标方法,目标方法有任何异常都会被catch,并被封装为dispatchException

  • 进入processDispatchResult方法,进行视图解析

  • 执行processHandlerException方法,处理异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if (this.handlerExceptionResolvers != null) {
    // 遍历所有处理器异常解析器来处理异常
    for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
    exMv = resolver.resolveException(request, response, handler, ex);
    if (exMv != null) {
    break;
    }
    }
    }

    • DefaultErrorAttributes将异常信息保存到request域,返回null视图
    • 当没有解析器能够处理异常时,会继续向上抛出异常,底层会转达到/error
  • /error请求被BasicErrorController处理

    • 解析错误视图,遍历所有ErrorViewResolver进行解析
    • DefaultErrorViewResolver会根据响应状态码获取返回页面名称(先根据精确的状态码查找页面,若没有再通过模糊的,如4xx、5xx进行查找),并拼接为地址
    • 模板引擎最终响应这个页面
自定义异常处理方式
  • 自定义错误页

    • error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
  • @ControllerAdvice+@ExceptionHandler处理全局异常; ExceptionHandlerExceptionResolver 来处理

    1
    2
    3
    4
    5
    6
    7
    8
    @ControllerAdvice
    public class CustomExceptionHandler {
    @ExceptionHandler({CustomException.class})
    @ResponseBody
    public Result handleCustom(Exception exception) {
    return Result.fail(exception.getMessage());
    }
    }
  • @ResponseStatus+自定义异常 ; ResponseStatusExceptionResolver来处理 ,获取@ResponseStatus注解的信息,然后底层调用 response.sendError(statusCode, resolvedReason);tomcat发送的/error

    1
    2
    3
    4
    @ResponseStatus(value = HttpStatus.BAD_GATEWAY, reason = "访问太频繁")
    public class TooManyRequestException extends RuntimeException {
    private static final long serialVersionUID = 7699893349483938519L;
    }
  • Spring底层的异常,如参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常

  • 自定义实现 HandlerExceptionResolver接口来处理异常;可以作为默认的全局异常处理规则(优先级需要调高)

  • 实现接口ErrorViewResolver来自定义错误视图处理

    • 如ErrorMvcAutoConfiguration中配置了DefaultErrorViewResolver,通过错误码拼接为地址,渲染对应模板文件在呈现给客户端

Web原生组件注入

JavaWeb原生组件就是Servlet、Filter、Listener

使用注解

当使用嵌入式容器时,可以通过使用 @ServletComponentScan 来启用对 @WebServlet@WebFilter@WebListener 注解的类的自动注册

这种方式注册的servlet不会经过spring的拦截器

使用RegistrationBean

可以使用 ServletRegistrationBeanFilterRegistrationBeanServletListenerRegistrationBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}

DispatchServlet注册的原理

  • DispatcherServletAutoConfiguration类中配置了名称为dispatcherServletDispatcherServlet的Bean
  • DispatcherServletRegistrationConfiguration中通过RegistrationBean的方式注册了dispatcherServlet

数据访问

SQL

JDBC

添加JDBC场景依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>

这个场景包引入了数据库连接池、jdbc、事务等包,但是数据库驱动需要根据项目情况手动引入

添加数据库对应版本的驱动包

1
2
3
4
5
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>

相关自动配置类:

  • DataSourceAutoConfiguration:数据源的自动配置
  • DataSourceTransactionManagerAutoConfiguration:事务管理器的自动配置
  • JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置
  • JndiDataSourceAutoConfiguration: jndi的自动配置
  • XADataSourceAutoConfiguration:分布式事务相关的

添加连接配置和连接池配置

1
2
3
4
5
6
7
8
9
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/tmp?useSSL=false&useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true
username: root
password: 123456
hikari:
minimum-idle: 5
maximum-pool-size: 50
更换连接池

方式一:

添加连接池依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>

添加配置类来覆盖默认数据源配置

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class DruidDataSourceConfig {
@ConfigurationProperties("spring.datasource") // 复用yml文件中的连接配置
@Bean
public DataSource dataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setMinIdle(5);
druidDataSource.setMaxActive(50);
return druidDataSource;
}
}

方式二:

添加连接池场景包

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>

这个场景包除了引入了方式一的依赖,还引入了自动配置包

自动配置类为DruidDataSourceAutoConfigure,导入了一下4个配置

  • DruidSpringAopConfiguration:用于监控SpringBean
  • DruidStatViewServletConfiguration:配置StatViewServlet(监控页面)
  • DruidWebStatFilterConfiguration:配置WebStatFilter
  • DruidFilterConfiguration:所有Druid自己filter的配置

添加连接配置和连接池配置

1
2
3
4
5
6
7
8
9
10
11
12
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/tmp?useSSL=false&useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true
username: root
password: 123456
druid:
min-idle: 5
max-active: 50
stat-view-servlet: # 开启监控页面
enabled: true
login-username: root
login-password: 123456
mybatis
  • 添加场景包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
    </dependency>

  • 修改相关配置

    1
    2
    3
    4
    mybatis:
    mapper-locations: classpath:mapper/*.xml
    configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名映射
  • 编写mapper接口,接口加上@Mapper注解,mybatis会自动扫描加上此注解的类

    • 使用@MapperScan(basePackages="xxx")注解,指定mapper接口包路径,就不需要在每个mapper接口上添加@Mapper注解了
  • 将mapper.xml文件放置对应目录下,与配置文件中mapper-locations描述的路径保持一致

mybatis-plus
  • 添加场景包

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

  • 修改相关配置

    1
    2
    3
    4
    5
    mybatis-plus:
    # mapper-locations: classpath:mapper/*.xml # 有默认配置可以不配置
    configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名映射
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler # 枚举类处理
  • 分页插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Configuration
    public class MybatisPlusConfig {
    /**
    * 开启分页插件
    * @return
    */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
    }
    }

Redis

引入与配置
  • 添加场景包

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

​ 默认引入lettuce作为redis客户端

  • 添加配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    redis:
    host: 192.168.0.123
    port: 46379
    password: 123456
    database: 1
    lettuce:
    pool:
    min-idle: 5
    max-active: 50
自动配置
  • RedisAutoConfiguration:redis的自动配置类
    • 会导入LettuceConnectionConfiguration和JedisConnectionConfiguration,分别表示lettuce和jedis客户端的配置
    • 注册了一个RedisTemplate<Object, Object>,键值都是object类型
    • 注册了一个StringRedisTemplate,键值都是字符串类型
  • RedisProperties:配置属性类
使用jedis
  • 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    </dependency>
  • 修改配置文件

    1
    2
    3
    spring:
    redis:
    client-type: jedis
使用

配置键值的序列化与反序列化器:

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
public class RedisSerializeConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 日期序列化处理
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule())
.registerModule(new ParameterNamesModule());
// 存储java的类型,方便反序列化,没有这行,将存储为纯json字符串
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
public class RedisTemplateTest {
@Autowired
private RedisTemplate redisTemplate;
static User user;
@BeforeAll
static void prepare() {
user = new User();
user.setId(46379);
user.setName("redis");
}
@Test
void testUserSetGet() {
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
ops.set("junit:user:" + user.getId(), user);
Object o = ops.get("junit:user:" + user.getId());
Assertions.assertNotNull(o);
}
}

单元测试

JUnit5 的变化

Spring Boot 2.2.0 版本开始引入 JUnit5 作为单元测试默认库

JUnit5由三个不同的子项目组成,JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

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

注意:SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容junit4需要自行引入(不能使用junit4的功能 @Test)

引入

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

使用方式示例

1
2
3
4
5
6
7
8
9
@SpringBootTest
public class RedisTemplateTest {
@BeforeAll
static void prepare() {
}
@Test
void test1() {
}
}

常用注解

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

断言

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证

这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法

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

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

1
2
3
4
@Test
void arrayEquals() {
assertArrayEquals(new int[]{1, 2}, new int[]{1, 2});
}
组合断言

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

1
2
3
4
5
6
7
@Test
void all() {
assertAll(
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}
异常断言

用于测试方法抛出异常

1
2
3
4
@Test
void exception() {
assertThrows(ArithmeticException.class, () -> System.out.println(1 % 0));
}
超时断言

用于测试方法超时时间

1
2
3
4
5
@Test
void timeout() {
// 测试方法超过1s时不通过
assertTimeout(Duration.ofSeconds(1), () -> Thread.sleep(100));
}
快速失败

通过 fail 方法直接使得测试失败

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

前置条件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
private final String environment = "DEV";
@Test
void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(Objects.equals(this.environment, "PROD"));
}
@Test
void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}

assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。

assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

嵌套测试

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@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());
}
}
}
}

参数化测试

利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码

实现ArgumentsProvider接口,任何外部文件都可以作为它的入参

常用注解
  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  • @NullSource: 表示为参数化测试提供一个null的入参
  • @EnumSource: 表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}

@ParameterizedTest
@MethodSource("produce") // 指定产生数据的方法名
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}

static Stream<String> produce() {
return Stream.of("apple", "banana");
}

指标监控

简介

SpringBoot Actuator,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。

使用

  • 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  • 修改配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    management:
    endpoints:
    enabled-by-default: true # 暴露所有端点信息
    web:
    exposure:
    include: '*' # 以web方式暴露
    endpoint:
    health: # 单独配置健康端点
    show-details: always # 显示详细详细

常用端点

ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
mappings 显示所有@RequestMapping路径列表。
scheduledtasks 显示应用程序中的计划任务。
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown 使应用程序正常关闭。默认禁用。
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump 执行线程转储。

若应用程序是Web应用程序,则可以使用以下附加端点:

ID 描述
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile 返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

Health Endpoint

Health Endpoint返回当前服务的健康状态,端点开启show-details: always后返回应用包含的一系列组件的健康状态

Metrics Endpoint

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

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

通过访问/actuator/metrics查看所有的指标名称

再通过访问/actuator/metrics/{requiredMetricName}查看指标的具体信息

自定义端点信息

自定义健康信息

需要实现HealthIndicator接口或者继承AbstractHealthIndicator抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class PMOpenHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// 实现健康检查逻辑
Map<String, Object> healthInfo = new HashMap<>();
if (DateUtil.isAM(new Date())) {
builder.status(Status.OUT_OF_SERVICE);
healthInfo.put("message", "上午暂停服务");
} else {
builder.up();
healthInfo.put("message", "正常服务中");
}
builder.withDetails(healthInfo);
}
}

查看健康状态

自定义info信息

方式一:

在配置文件中配置

1
2
3
info:
projectName: @project.artifactId@ #使用@可以获取maven的pom文件值
projectVersion: @project.version@

方式二:

实现InfoContributor接口

1
2
3
4
5
6
7
8
@Component
public class ApplicationInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("name", "jdap")
.withDetail("version", "1.0.0");
}
}
自定义指标信息

比如说我想要获取文件上传次数的指标,可以使用以下方式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("file")
public class FileController {
private Counter counter;
public FileController(MeterRegistry meterRegistry) {
counter = meterRegistry.counter("file.upload.counter");
}
@PostMapping("upload")
public String upload(@RequestPart MultipartFile[] files) throws IOException {
counter.increment();
// do upload
return "success";
}
}

在其他场景中也可以使用以下方式

1
2
3
4
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}

自定义端点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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....");
}
}

可视化

使用spring-boot-admin实现可视化,参考文档

监控可视化平台服务端

另起一个项目作为监控可视化平台

引入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 指标监控可视化 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.7.9</version>
</dependency>

配置服务端口

1
2
server:
port: 8124
客户端

客户端就是被监控的项目

引入依赖

1
2
3
4
5
6
<!-- 指标监控可视化客户端 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.7.9</version>
</dependency>

修改配置

1
2
3
4
5
6
7
spring:
boot:
admin:
client:
url: http://127.0.0.1:8124 # 服务端地址
application:
name: jdap # 此名称用于服务端显示作为标识

原理解析

Profile功能

用于多环境适配

application-profile功能
  • 默认配置文件 application.yaml;任何时候都会加载

  • 指定环境配置文件 application-{env}.yaml

    • application-dev.yml
    • application-prod.yml
  • 激活指定环境

    • 配置文件激活,在默认配置文件中进行以下配置

      1
      2
      3
      spring:
      profiles:
      active: dev
    • 命令行激活:java -jar xxx.jar –spring.profiles.active=dev

  • 默认配置与环境配置同时生效

  • 同名配置项,profile配置优先

@Profile条件装配功能

标记类在指定环境下才生效

1
2
3
4
@Configuration(proxyBeanMethods = false)
@Profile("prod")
public class ProductionConfiguration {
}
profile分组
1
2
3
4
5
6
7
8
9
10
spring:
profiles:
active: dev
group:
prod:
- prod1
- prod2
dev:
- dev1
- dev2

spring.profiles.active指定组名来激活环境组

外部化配置

外部配置源

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

配置文件查找位置

以下位置的配置文件中,若出现相同配置项,从下到上依次覆盖

  • classpath 根路径
  • classpath 根路径下config目录
  • jar包当前目录
  • jar包当前目录的config目录
  • /config子目录的直接子目录
配置文件加载顺序
  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
总结

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

SpringBoot原理

SpringBoot启动过程
  • SpringApplication.run(启动类.class, args)

  • new SpringApplication(primarySources).run(args):创建一个SpringApplication并运行

  • SpringApplication的创建

    • 保存一些信息

    • 获取web应用类型,servlet或webflux

    • bootstrapRegistryInitializers:获取引导注册初始化器

      • META-INF/spring.factories文件中读取org.springframework.boot.BootstrapRegistryInitializer类型的类
    • initializers:获取应用初始化器

      • META-INF/spring.factories文件中读取org.springframework.context.ApplicationContextInitializer类型的类

    • listeners:获取应用监听器

      • META-INF/spring.factories文件中读取org.springframework.context.ApplicationListener类型的类

    • deduceMainApplicationClass():推导出main方法所在类

  • SpringApplication的运行

    • 记录应用启动时间

    • createBootstrapContext():创建引导上下文

      • 创建DefaultBootstrapContext对象
      • 遍历bootstrapRegistryInitializers,调用initialize方法初始化上下文环境设置
    • configureHeadlessProperty():让当前应用进行headless模式,java.awt.headless

    • getRunListeners():获取运行监听器

      • META-INF/spring.factories文件中读取org.springframework.boot.SpringApplicationRunListener类型的类

    • listeners.starting(bootstrapContext, this.mainApplicationClass):遍历上面获取的所有运行监听器,调用starting方法,相当于通知关心系统启动事件的监听器系统已启动

    • applicationArguments:保存命令行参数

    • prepareEnvironment():准备环境信息

      • getOrCreateEnvironment():获取或创建一个环境信息

      • configureEnvironment():配置环境信息

        1
        2
        3
        4
        5
        6
        7
        8
        9
        protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
        if (this.addConversionService) {
        // 设置类型转换器
        environment.setConversionService(new ApplicationConversionService());
        }
        // 读取所有的配置源的配置属性值
        configurePropertySources(environment, args);
        configureProfiles(environment, args);
        }
      • ConfigurationPropertySources.attach(environment):绑定环境信息

      • listeners.environmentPrepared(bootstrapContext, environment):遍历运行监听器调用environmentPrepared方法,通知所有运行监听器当前环境已准备完毕

    • 打印banner

    • createApplicationContext():创建IOC容器

      • 根据项目类型(servlet)创建容器
      • 当前会创建AnnotationConfigServletWebServerApplicationContext
    • prepareContext():准备IOC容器信息

      • context.setEnvironment(environment):保存环境信息到容器中
      • postProcessApplicationContext():IOC容器后置处理
      • applyInitializers():应用初始化器
        • 遍历创建阶段获取的ApplicationContextInitializer,调用initialize方法对IOC容器进行初始化扩展
      • listeners.contextPrepared(context):遍历运行监听器调用contextPrepared方法,通知所有运行监听器IOC容器已准备完毕
      • listeners.contextLoaded(context):遍历运行监听器调用contextLoaded方法,通知所有运行监听器IOC容器已加载完毕
    • refreshContext():刷新IOC容器

      • 创建容器中的所有组件
    • 打印启动耗时

    • listeners.started(context, timeTakenToStartup):遍历运行监听器调用started方法,通知所有运行监听器当前项目已启动

    • callRunners():调用所有的runner

      • 从IOC容器中获取org.springframework.boot.ApplicationRunner和org.springframework.boot.CommandLineRunner类型的runner对象
      • 将所有runner按order排序
      • 遍历所有runner调用run方法
    • 启动成功或失败

      • 若启动没有异常
        • listeners.ready(context, timeTakenToReady):遍历运行监听器调用ready方法,通知所有运行监听器当前项目已准备就绪
      • 若启动有异常
        • 调用handleRunFailure方法,处理运行失败情况
          • isteners.failed(context, exception):遍历运行监听器调用failed方法,通知所有运行监听器当前项目启动失败
SpringBoot启动过程中的关键组件
  • org.springframework.context.ApplicationContextInitializer
  • org.springframework.context.ApplicationListener
  • org.springframework.boot.SpringApplicationRunListener
  • org.springframework.boot.ApplicationRunner
  • org.springframework.boot.CommandLineRunner
自定义关键组件注意事项

继承对应的接口,实现对应的方法

除了上面的两个runner,其他类都需要在再META-INF/spring.factories文件中声明

1
2
3
4
5
6
7
8
org.springframework.context.ApplicationContextInitializer=\
com.zhuweitung.jdap.listener.MyApplicationContextInitializer

org.springframework.context.ApplicationListener=\
com.zhuweitung.jdap.listener.MyApplicationListener

org.springframework.boot.SpringApplicationRunListener=\
com.zhuweitung.jdap.listener.MySpringApplicationRunListener

上面的两个runner需要加上@Component注解注册为组件

1
2
3
4
5
6
7
8
@Slf4j
@Order(1)
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("MyApplicationRunner run, args is {}", Arrays.toString(args.getSourceArgs()));
}
1
2
3
4
5
6
7
8
9
@Slf4j
@Order(2)
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("MyCommandLineRunner run, args is {}", Arrays.toString(args));
}
}

SpringBoot2知识学习及查漏补缺
https://blog.kedr.cc/posts/1292434261/
作者
zhuweitung
发布于
2021年6月17日
许可协议