0%

SpringBoot 核心-核心功能

SpringBoot 核心功能

yaml配置文件

配置的时候,如果存在多个文件,则优先级为:properties>yml>yaml

基本语法

  • key:value
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格(IDEA会将tab换成4个空格)
  • 缩进的空格数不重要,只要相同层级的元素左对齐可
  • '#'表示注释
  • ''与""表示字符串内容会被转义()/不转义();常规字符串是否加引号,加''还是加""没有区别

数据类型

  • 字面量:单个的、不可再分的值。date、boolean、string、number、null

    1
    k:v
  • 对象:键值对的集合。map、hash、set、object

    1
    2
    3
    4
    5
    6
    k: {k1:v1, k2:v2, k3:v3}
    # 或
    k:
    k1: v1
    k2: v2
    k3: v3
  • 数组:一组按次序排列的值。array、list、queue

    1
    2
    3
    4
    5
    6
    k: [v1, v2, v3]
    # 或
    k:
    - v1
    - v2
    - v3

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@ConfigurationProperties(prefix="person")
@Data
public class Person {

private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}

@Data
public class Pet {
private String name;
private Double weight;
}
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
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]

自定义配置提示

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自动配置

  • ContentNegotiatingViewResolverBeanNameViewResolver:内容协商视图解析器和BeanName视图解析器
  • 静态资源
  • 自动注册Converter, GenericConverter, 和Formatter
  • 支持HttpMessageConverters
  • 自动注册MessageCodesResolver(国际化用)
  • 静态index.html页支持
  • 自定义Favicon
  • 自动使用ConfigurableWebBindingInitializer

不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则

声明 WebMvcRegistrations 改变默认底层组件

使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC

简单功能分析

静态资源访问

  1. 静态资源目录

    只要静态资源放在类路径下:/static(或/public/resourcesMETA-INF/resources)

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

    原理:静态映射/**

    请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面

    改变默认的静态资源路径:

    1
    2
    3
    4
    spring:
    web:
    resources:
    static-locations: classpath:abc
  2. 静态资源访问前缀

    默认无前缀,配置前缀:

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

    访问:当前项目根路径/+static-path-pattern+静态资源名

  3. webjar

    自动映射/webjar/**

    WebJars - Web Libraries in Jars

    例:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.5.1</version>
    </dependency>

    可通过:http://127.0.0.1:8080/webjars/jquery/3.5.1/jquery.js访问,后面地址要按照依赖里面的包路径

欢迎页支持

静态资源路径下index.html

  • 可以配置静态资源路径

  • 但不可以配置静态资源的访问前缀,否则导致index.html不能被默认访问

    1
    2
    3
    spring:
    # mvc:
    # static-path-pattern: /res/** 这个会导致欢迎页功能失效
  • controller能处理/index

自定义Favicon

favicon.ico放在静态资源目录下即可

1
2
3
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效

静态资源配置原理

  • SpringBoot启动默认加载xxxxAutoConfiguration类(自动配置类),其中会包含与Web开发有关的自动配置类

  • SpringMVC功能的自动配置类:WebMvcAutoConfiguration,生效(条件均满足):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @AutoConfiguration(
    after = {DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class}
    )
    @ConditionalOnWebApplication(
    type = Type.SERVLET
    )
    @ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
    @ConditionalOnMissingBean({WebMvcConfigurationSupport.class}) // 说明通过定义WebMvcConfigurationSupport可以全面接管Spring MVC
    @AutoConfigureOrder(-2147483638)
    public class WebMvcAutoConfiguration {
  • 给容器中配置了:

    1
    2
    3
    4
    5
    6
    7
    @Configuration(
    proxyBeanMethods = false
    )
    @Import({EnableWebMvcConfiguration.class})
    @EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
  • 配置文件的相关属性和什么进行了绑定

    • WebMvcProperties:spring.mvc
    • WebProperties:spring.web
  1. 配置类只有一个有参构造器,有参构造器所有参数的值都会从容器中确定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // WebProperties webProperties:获取spring.web绑定的所有的值的对象
    // WebMvcProperties mvcProperties:获取spring.mvc绑定的所有的值的对象
    // ListableBeanFactory beanFactory:Spring的BeanFactroy
    // HttpMessageConverters:找到HttpMEssageConverters
    // ResourceHandlerRegistrationCustomizer:找到资源处理器的自定义器
    // DispatcherServletPath:
    // ServletRegistrationBean:给应用注册原生Servlet、Filter、Listener等
    public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
    this.resourceProperties = webProperties.getResources();
    this.mvcProperties = mvcProperties;
    this.beanFactory = beanFactory;
    this.messageConvertersProvider = messageConvertersProvider;
    this.resourceHandlerRegistrationCustomizer = (ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
    this.dispatcherServletPath = dispatcherServletPath;
    this.servletRegistrations = servletRegistrations;
    this.mvcProperties.checkConfiguration();
    }
  2. 资源处理的默认规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
    logger.debug("Default resource handling disabled");
    } else {
    // webjars的规则
    this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
    this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
    registration.addResourceLocations(this.resourceProperties.getStaticLocations());
    if (this.servletContext != null) {
    ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
    registration.addResourceLocations(new Resource[]{resource});
    }

    });
    }
    }

    静态资源默认位置:

    1
    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
  3. 欢迎页处理规则

    1
    2
    3
    4
    5
    6
    7
    8
    // HandlerMapping:处理器映射,保存了每个Handler能处理哪些请求
    @Bean
    public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
    welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
    return welcomePageHandlerMapping;
    }

请求参数处理

请求映射

  1. rest使用与原理

    • @xxxMapping:表示对应的Rest请求

    • Rest风格(使用HTTP请求方式动词来表示对资源的操作)

    • 核心Filter:HiddenHttpMethodFilter

      • 用法:表单method=post,隐藏域_method=put

      • SpringBoot中手动开启

        1
        2
        3
        4
        5
        spring:
        mvc:
        hiddenmethod:
        filter:
        enabled: true
        1
        2
        3
        4
        5
        6
        7
        8
        9
        @Bean
        @ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
        @ConditionalOnProperty(
        prefix = "spring.mvc.hiddenmethod.filter",
        name = {"enabled"}
        )
        public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
        }
    • 修改隐藏域_method为自定义名称,如_m

      1
      2
      3
      4
      5
      6
      @Bean
      public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
      HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
      hiddenHttpMethodFilter.setMethodParam("_m");
      return hiddenHttpMethodFilter;
      }
    • Rest原理(表单提交时使用REST风格时)

      • 表单提交会带上_method=PUT
      • 请求过来被hiddenHttpMethodFilter拦截
        • 判断请求是否正常,并且是POST请求
        • 获取_method的值
        • 兼容:PUTDELETEPATCH请求
        • 对原生request(post),通过装饰器模式requestWrapper重写getMethod方法,返回传入的值
        • 过滤器链放行时就用requestWrapper
    • 如果使用客户端工具发送REST请求则无需filter

  2. 请求映射原理

    DispatcherServlet继承树:

    • HttpServlet
      • HttpServletBean
        • FrameworkServlet:重写了doGet/doPost等方法,内部调用processRequest方法,processRequest方法再调用doService方法
          • DispatcherServlet:重写了doService方法,内部调用doDispatch方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
    try {
    ModelAndView mv = null;
    Exception dispatchException = null;

    try {
    processedRequest = this.checkMultipart(request);
    multipartRequestParsed = processedRequest != request;
    // 找到当前请求对应的Handler(哪个Controller的哪个方法)
    mappedHandler = this.getHandler(processedRequest);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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;
    }

    HandlerMappings:处理器映射

    其中:RequestMappingHandlerMapping保存了所有@RequestMapping和handle的映射规则

    所有请求映射都在HandleMapping中

    • SpringBoot自动配置了欢迎页的WelcomePageHandlerMapping
    • SpringBoot自动配置了默认的RequestMappingHandlerMapping
    • 请求进入后逐个尝试所有的HandlerMapping看是否有请求信息
      • 如果有就找到这个请求对应的Handler
      • 没有就继续找下一个HandlerMapping
    • 也可以自定义HandlerMapping

普通参数与基本注解

注解:

  • @PathVariable:路径参数,路径的{}中的参数,所有路径参数的集合可用Map

  • @RequestHeader:请求头,所有请求头的集合可用Map

  • @ModelAttribute

  • @RequestParam:请求参数,集合类型可用List,所有请求参数的集合可用Map

  • @MatrixVariable:矩阵变量

    • 矩阵变量应该绑定在路径变量中

    • 原:/cars/{path}?xxx=xxx&aaa=bbb;queryString查询字符串。@RequestParam

    • 矩阵变量:/cars/path;xxx=xxx;aaa=bbb;矩阵变量

    • 如果Cookie被禁用如何获取Session?

      • url重写:/abc;jsessionid=xxxx 把cookie的值使用矩阵变量方式进行传递
    • SpringBoot需要手动开启矩阵变量功能

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      // 继承
      @Configuration(proxyBeanMethods = false)
      public class WebConfig implements WebMvcConfigurer {
      @Override
      public void configurePathMatch(PathMatchConfigurer configurer) {
      UrlPathHelper urlPathHelper = new UrlPathHelper();
      // 设置false,表示不移除分号后面的内容,矩阵变量才能生效
      urlPathHelper.setRemoveSemicolonContent(false);
      configurer.setUrlPathHelper(urlPathHelper);
      }
      }
      // 或者定义Bean
      @Bean
      public WebMvcConfigurer webMvcConfigurer(){
      return new WebMvcConfigurer() {
      @Override
      public void configurePathMatch(PathMatchConfigurer configurer) {
      UrlPathHelper urlPathHelper = new UrlPathHelper();
      urlPathHelper.setRemoveSemicolonContent(false);
      configurer.setUrlPathHelper(urlPathHelper);
      }
      };
      }
    • 例:

      1
      2
      3
      @GetMapping("/cars/{bossId}/{empId}")
      public Map carsSell(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge,
      @MatrixVariable(value = "age", pathVar = "empId") Integer empAge)
  • @CookieValue:获取Cookie,可以用String或Cookie类型参数接收

  • @RequestBody:获取表单数据

  • @RequestAttribute:获取Reqeust域属性

Servlet API:

  • WebRequest
  • ServletRequest
  • MultipartRequest
  • HttpSession
  • javax.servlet.http.PushBuilder
  • Principal
  • InputStream
  • Reader
  • HttpMethod
  • Locale
  • TimeZone
  • ZoneId

ServletRequestMethodArgumentResolver可解析以上的部分参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}

复杂参数:

  • Map, Model:数据会被放在reqeust的请求域,相当于使用request.setAttribute(),内部的数据也就可以直接通过request.getAttribute()获取
    • Map类型的参数会返回ModelAndViewContainer类中的getModel()对应的是BindingAwareModelMap,它既是Model也是Map
    • Model类型的参数底层的调用和Map类型一样,并且是同一个对象
  • Errors/BindingResult
  • RedirectAttributes:重定向携带数据
  • ServletResponse:原生response
  • SessionStatus
  • UriComponentsBuilder
  • ServletComponentsBuilder

自定义对象参数:

  • 可以自动类型转换与格式化,可以级联封装

POJO封装过程

  • 使用ServletModelAttributeMethodProcessor进行解析

参数处理原理

  • HandleMapping中找到能处理请求的Handler(哪个Controller中的哪个method)
  • 为当前Handler找一个适配器HandlerAdapter,RequestMappingHandlerAdapter
  • 通过HandlerAdapter执行目标方法
HandlerAdapter
  • RequestMappingHandlerAdapter:支持方法上标注@RequestMapping
  • HandlerFunctionAdapter:支持函数式编程
  • HttpReqeustHandlerAdapter
  • SimpleControllerHandlerAdapter
执行目标方法
1
2
// DispatcherServlet中的doDispatch()
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
1
2
3
4
5
6
7
// RequestMappingHandlerAdapter类中的handleInternal()
mav = invokeHandlerMethod(request, response, handlerMethod);
// ServletInvocableHandlerMethod类中的invokeAndHandle()
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// 获取方法的参数值
// invokeForRequest方法内执行
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
1

参数解析器

确定将要执行的目标方法的每个参数值是什么

SpringMVC目标方法能写多少种参数类型就取决于参数解析器

工作步骤:

  • 当前解析器是否支持解析这种参数
  • 如果支持则调用解析方法(resolveArgument)

返回值处理器

决定能返回哪些类型的值

如何确定目标方法的每一个参数值
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
// InvocableHandlerMethod类中
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;
}
  1. 逐个判断所有参数解析器是否支持解析对应的参数,找到对应的这个解析器,内部判断机制是通过判断是否标注对应的注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // this.resolvers.supportsParameter(parameter)
    @Nullable
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
    for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
    if (resolver.supportsParameter(parameter)) {
    result = resolver;
    // 缓存
    this.argumentResolverCache.put(parameter, result);
    break;
    }
    }
    }
    return result;
    }
  2. 解析参数的值

    1
    args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
  3. 自定义类型参数,封装POJO

    使用ServletModelAttributeMethodProcessor

    1
    2
    3
    4
    5
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
    return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
    (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()))); // 判断是否为非简单类型
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    @Override
    @Nullable
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

    Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
    Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");

    String name = ModelFactory.getNameForParameter(parameter);
    ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
    if (ann != null) {
    mavContainer.setBinding(name, ann.binding());
    }

    Object attribute = null;
    BindingResult bindingResult = null;

    if (mavContainer.containsAttribute(name)) {
    attribute = mavContainer.getModel().get(name);
    }
    else {
    // Create attribute instance
    try {
    attribute = createAttribute(name, parameter, binderFactory, webRequest);
    }
    catch (BindException ex) {
    if (isBindExceptionRequired(parameter)) {
    // No BindingResult parameter -> fail with BindException
    throw ex;
    }
    // Otherwise, expose null/empty value and associated BindingResult
    if (parameter.getParameterType() == Optional.class) {
    attribute = Optional.empty();
    }
    else {
    attribute = ex.getTarget();
    }
    bindingResult = ex.getBindingResult();
    }
    }

    if (bindingResult == null) {
    // Bean property binding and validation;
    // skipped in case of binding failure on construction.
    // 外部数据绑定器,将请求参数的值绑定到JavaBean
    WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    if (binder.getTarget() != null) {
    if (!mavContainer.isBindingDisabled(name)) {
    bindRequestParameters(binder, webRequest);
    }
    validateIfApplicable(binder, parameter);
    if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
    throw new BindException(binder.getBindingResult());
    }
    }
    // Value type adaptation, also covering java.util.Optional
    if (!parameter.getParameterType().isInstance(attribute)) {
    attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
    }
    bindingResult = binder.getBindingResult();
    }

    // Add resolved attribute and BindingResult at the end of the model
    Map<String, Object> bindingResultModel = bindingResult.getModel();
    mavContainer.removeAttributes(bindingResultModel);
    mavContainer.addAllAttributes(bindingResultModel);

    return attribute;
    }

    其中重点在于:

    1
    2
    //  外部数据绑定器,将请求参数的值绑定到JavaBean(利用converters转换器将请求中的所有数据转换成对应类型后再利用反射绑定到对象)
    WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

    GenericConversionService:在设置每一个值时,找它里面的所有converter中哪个可以将这个数据类型转换到指定的类型

将来就可以给WebDataBinder里面放自己需要的Converter

自定义Converter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
if (!StringUtils.isEmpty(source)) {
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(split[1]);
return pet;
}
return null;
}
});
}
}
目标方法执行完成

将所有的数据都放在ModelAndViewContainer中,包含:

  • 要去的页面地址View
  • Model数据
处理分发结果
1
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
1
2
//
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
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
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

// Expose the model object as request attributes.
// 暴露模型作为请求域属性,即将Model中的属性通过request.setAttribute()设置到request域中
exposeModelAsRequestAttributes(model, request);

// Expose helpers as request attributes, if any.
exposeHelpers(request);

// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);

// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}

// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}

else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
rd.forward(request, response);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 暴露模型作为请求域属性,即将Model中的属性通过request.setAttribute()设置到request域中
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {

model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}

数据响应与内容协商

响应JSON

  1. jackson.jar+@ResponseBody

    在pom.xml中引入spring-boot-starter-web时自动引入了json启动器spring-boot-starter-json,实际上使用的是jackson

    然后就能给前端自动返回JSON数据

    1. 返回值解析器

      返回值解析器
      返回值解析器
      1
      2
      3
      4
      5
      try {
      // 处理返回值
      this.returnValueHandlers.handleReturnValue(
      returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Override
      public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

      HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
      if (handler == null) {
      throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
      }
      handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 最终处理返回值
      @Override
      public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
      throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

      mavContainer.setRequestHandled(true);
      ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
      ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

      // Try even with null return value. ResponseBodyAdvice could get involved.
      // 使用消息转换器进行写出操作
      writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
      }
    2. 返回值解析器原理

      1. 返回值处理器判断是否支持这种类型返回值supportsReturnType
      2. 返回值处理器调用handleReturnValue进行处理
      3. RequestResponseBodyMethodProcessor可以处理返回值标了@ResponseBody注解的
        • 利用MessageConverter进行处理,将数据写为json
          • 内容协商(浏览器默认会以请求头的方式告诉服务器能接受什么样的内容类型)
          • 服务器最终根据自身的能力决定服务器能生产什么样内容类型的数据
          • SpringMVC会逐个遍历所有容器底层的HttpMessageConverter,找到能处理的Converter
            • 得到MappingJackson2HttpMessageConverter可以将对象写为json
            • 利用MappingJackson2HttpMessageConverter将对象转为json再写出去
  2. SpringMVC支持哪些返回值

    • ModelAndView
    • Model
    • View
    • ResponseEntity
    • ResponseBodyEmitter
    • StreamingResponseBody
    • HttpEntity
    • HttpHeaders
    • Callable
    • DeferredResult
    • ListenableFuture
    • CompletionStage
    • WebAsyncTask
    • @ModelAttribute且为对象类型
    • @ResponseBody,对应处理器为:RequestResponseBodyMethodProcessor
  3. HTTPMessageConverter原理

    1. MessageConverter接口规范

      • 判断是否支持此Class类型的对象转为MediaType类型的数据
    2. 默认的MessageConverter

      • ByteArrayHttpMessageConverter:只支持byte类型
      • StringHttpMessageConverter:String类型
      • StringHttpMessageConverter:String类型
      • ResourceHttpMessageConverter:Resource
      • ResourceRegionHttpMessageConverter:ResourceRegion
      • SourceHttpMessageConverter:DOMSource.class、SAXSource.class、StAXSource.class、StreamSource.class、Source.class
      • AllEncompassingFormHttpMessageConverter:MultiValueMap
      • MappingJackson2HttpMessageConverter:全支持,直接返回true
      • MappingJackson2HttpMessageConverter:全支持,直接返回true
      • Jaxb2RootElementHttpMessageConverter:支持注解方式xml处理

    最终MappingJackson2HttpMessageConverter将对象转为JSON(利用底层的jackson的objectMapper进行转换)

内容协商

根据客户端接收能力不同,返回不同媒体类型的数据

  1. 引入xml依赖

    1
    2
    3
    4
    <dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
  2. postman分别测试返回json和返回xml

    只需要改变请求头中Accept字段(application/json或application/xml),HTTP协议规定的,告诉服务器本客户端可以接收的数据类型

  3. 开启浏览器参数方式内容协商功能

    为了方便内容协商,开启基于请求参数的内容协商功能

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

    然后就可以通过http://127.0.0.1:8080/params?format=json获取json数据;通过http://127.0.0.1:8080/params?format=xml获取xml数据

    确定客户端接收什么样的内容类型:

    1. Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
    2. 最终进行内容协商返回给客户端json即可
  4. 内容协商原理

    1. 判断当前响应头中是否已经有确定的媒体类型(MedialType)
    2. 获取客户端支持接收的内容类型。(获取客户端Accept请求头字段):acceptableTypes = getAcceptableMediaTypes(request);
      • contentNegotiationManager:内容协商管理器,默认使用基于请求头的策略
      • HeaderContentNegotiationStrategy:确定客户端可以接收的内容类型
      • 当设置开启了浏览器参数方式内容协商功能后,就会多一个ParameterContentNegotiationStrategy:基于参数的内容类型协商策略
    3. 获取服务器可以产生的内容类型;List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
    4. 遍历循环所有当前系统的MessageConverter,看谁支持操作这个对象
    5. 找到支持操作Person的converter,把所有converter支持的媒体类型统计出来
    6. 客户端需要的内容类型(如:application/xml),而服务端的能产生的内容类型是一个列表(多种)
    7. 进行内容协商,选择最佳匹配媒体类型
    8. 用对应的converter将对象转为最佳匹配媒体类型
  5. 自定义MessageConverter

    实现多协议数据兼容

    1. @ResponseBody响应数据出去调用RequestResponseBodyMethodProcessor处理
    2. Processor处理方法返回值。通过MessageConverter处理
    3. 所有MessageConverter合起来可以支持各种媒体类型数据的操作(读、写)
    4. 内容协商找到最终的messageConverter
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
/**
* 自定义Converter
*/
public class CustomMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// 不支持读,即无法从参数中获取对应的数据
return false;
}

@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}

/**
* 服务器要统计所有MessageConverter都能写出哪些内容类型
* application/x-abc
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-abc");
}

@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}

@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// 自定义协议数据的写出
String data = person.getUserName() + ";" + person.getAge();
// 写出
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
// 自定义Converter,需求是将数据以:xxx;yyy的形式返回
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CustomMessageConverter());
}
/**
* 自定义内容协商策略,然后就可以通过http://127.0.0.1:8080/params?format=abc请求
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> map = new HashMap<>();
map.put("json", MediaType.APPLICATION_JSON);
map.put("xml", MediaType.APPLICATION_XML);
map.put("abc", MediaType.parseMediaType("application/x-abc"));
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(map);
HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy(); // 添加处理头的策略,解决失效问题
configurer.strategies(Arrays.asList(parameterStrategy, headerStrategy));
}
}

有可能添加的自定义功能会覆盖默认很多功能,导致一些默认功能失效,就需要自己添加对应的功能

视图解析与模板引擎

视图解析:SpringBoot默认不支持JSP,需要引入第三方模板引擎技术实现页面渲染

视图解析原理

  1. 目标方法处理的过程中,所有数据都会被放在ModelAndViewContainer里面,包括数据和视图地址

  2. 方法的参数是一个自定义类型对象(从请求参数中确定的),把它重新放在ModelAndViewContainer

  3. 任何目标方法执行完成以后都会返回ModelAndView(数据和视图地址)

  4. processDispatchResult处理分发结果(决定 页面该如何响应)

    render(mv, request, response)进行页面渲染逻辑

    • 根据方法的String返回值得到View对象(定义了页面的渲染逻辑)
      • 所有视图解析器尝试是否能根据当前返回值得到VIew对象
      • 根据返回值得到了View,如redirect:/main.html --> Thymeleaf视图解析器中new RedirectView()
      • ContentNegotiationViewResolver里面包含了其它所有的视图解析器,内部还利用其它所有视图解析器得到的视图对象
    • view.render(mv.getModelInternal(),request,response);视图对象调用自定义的render进行页面渲染工作
      • RedirectView如何渲染(重定向到一个页面)
        • 获取目标url地址
        • response.sendRedirect(encodedURL);

视图解析:

  • 返回值以forward:开始:new InternalResourceView(forwardUrl); request.getReqeustDispatcher(path).forward(request, response);:转发
  • 返回值以redirect:开始:new RedirectView()render就是重定向
  • 返回值是普通字符串:new ThymeleafView()

模板引擎-Thymeleaf

  1. 基本语法

    • 表达式

      表达式名字 语法 用途
      变量取值 ${...} 获取请求域、session域、对象等值
      选择变量 *{...} 获取上下文对象值
      消息 #{...} 获取国际化等值
      链接 @{...} 生成链接
      片段表达式 ~{...} jsp:include作用,引入公共页面片段
    • 字面量

      文本值:'some text';数字:0,12;布尔值:true/false

      空值:null

      变量:a,b,user...

    • 文本操作

      字符串拼接:+

      变量替换:|The name is ${name}|

    • 数学运算

      运算符:+,-,*,/,%

    • 布尔运算

      运算符:and, or

      一元运算:!, not

    • 比较运算

      比较:>,<,>=,<=(gt,lt,ge,le)

      等式:==,!=(eq, ne)

    • 条件运算

      If-then:(if)?(then)

      If-then-else:(if)?(then):(else)

      Default: (value)?:(defualtvalue)

    • 特殊操作

      无操作:_

  2. 设置属性值-th:attr

    设置单个值

    1
    2
    3
    4
    5
    6
    <form action="subscribe.html" th:attr="action=@{/subscribe}">
    <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
    </fieldset>
    </form>

    设置多个值

    1
    <img src="../../images/gtvglogo.png"  th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

    以上两个的代替写法

    1
    2
    <input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
    <form action="subscribe.html" th:action="@{/subscribe}">
  3. 迭代

    1
    2
    3
    4
    5
    <tr th:each="prod : ${prods}">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
    </tr>
    1
    2
    3
    4
    5
    <tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
    <td th:text="${prod.name}">Onions</td>
    <td th:text="${prod.price}">2.41</td>
    <td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
    </tr>
  4. 条件运算

    1
    2
    3
    <a href="comments.html"
    th:href="@{/product/comments(prodId=${prod.id})}"
    th:if="${not #lists.isEmpty(prod.comments)}">view</a>
    1
    2
    3
    4
    5
    <div th:switch="${user.role}">
    <p th:case="'admin'">User is an administrator</p>
    <p th:case="#{roles.manager}">User is a manager</p>
    <p th:case="*">User is some other thing</p>
    </div>
  5. 属性优先级

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
       ```

    #### thymeleaf使用

    1. 引入starter

    ```xml
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
  6. 自动配置的thymeleaf

    1
    2
    3
    4
    5
    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(ThymeleafProperties.class)
    @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
    @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
    public class ThymeleafAutoConfiguration { }

    自动配置好的策略:

    • 所有thymeleaf的配置值都在ThymeleafProperties
    • 配置好了SpringTemplateEngine
    • 配好了ThymeleafViewResolver
    • 使用者只需要直接开发页面
    1
    2
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
  7. 页面开发

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8">
    <title>Title</title>
    </head>
    <body>
    <h1 th:text="${msg}">哈哈</h1>
    <h2>
    <a href="www.atguigu.com" th:href="${link}">去百度</a> <br/>
    <a href="www.atguigu.com" th:href="@{/link}">去百度2</a>
    </h2>
    </body>
    </html>

拦截器

案例:

  • 实现HandlerInterceptor接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    /**
    * 登录检查为例
    */
    @Slf4j
    public class LoginInterceptor implements HandlerInterceptor {
    /**
    * 目标方法执行前
    * @param request current HTTP request
    * @param response current HTTP response
    * @param handler chosen handler to execute, for type and/or instance evaluation
    * @return
    * @throws Exception
    */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String requestURI = request.getRequestURI();
    log.info("拦截的请求路径是{}", requestURI);

    HttpSession session = request.getSession();
    Object loginUser = session.getAttribute("loginUser");
    if(loginUser!=null) return true;
    request.setAttribute("msg", "请先登录");
    request.getRequestDispatcher("/").forward(request, response);
    return false;
    }

    /**
    * 目标方法执行后
    * @param request current HTTP request
    * @param response current HTTP response
    * @param handler the handler (or {@link HandlerMethod}) that started asynchronous
    * execution, for type and/or instance examination
    * @param modelAndView the {@code ModelAndView} that the handler returned
    * (can also be {@code null})
    * @throws Exception
    */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    log.info("postHandle执行{}", modelAndView);
    }

    /**
    * 页面渲染后
    * @param request current HTTP request
    * @param response current HTTP response
    * @param handler the handler (or {@link HandlerMethod}) that started asynchronous
    * execution, for type and/or instance examination
    * @param ex any exception thrown on handler execution, if any; this does not
    * include exceptions that have been handled through an exception resolver
    * @throws Exception
    */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    log.info("afterCompletion执行异常", ex);
    }
    }
  • 配置拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration(proxyBeanMethods = false)
    public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginInterceptor())
    .addPathPatterns("/**") // 拦截所有请求
    .excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**"); //放行部分请求
    }
    }

原理

  1. 根据当前请求,找到HandlerExecutionChain(可以处理请求的Handler以及Handler的所有拦截器)
  2. 顺序执行所有拦截器的preHandle方法
    • 如果当前拦截器preHandle返回true,则执行下一个拦截器的preHandle
    • 如果当前拦截器preHandle返回false,则直接倒序执行所有已经执行的拦截器的afterCompletion
  3. 如果任何一个拦截器返回false,直接跳出,不执行目标方法
  4. 所有拦截器都返回true,则执行目标方法
  5. 倒序执行所有拦截器的postHandle方法
  6. 前面的步骤有任何异常都会直接倒序触发afterCompletion
  7. 页面成功渲染完成后,也会倒序触发afterCompletiont

文件上传

案例

  1. 页面表单

    1
    2
    3
    4
    <form method="post" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file"><br>
    <input type="submit" value="提交">
    </form>
  2. 文件上传代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @PostMapping("/upload")
    public String upload(@RequestPart("headerImg")MultipartFile headerImg,
    @RequestPart("photos") MultipartFile[] photos) throws IOException {
    if(!headerImg.isEmpty()){
    // 保存到文件服务器,OSS服务器
    String originalFilename = headerImg.getOriginalFilename();
    headerImg.transferTo(new File("F:\\cache\\"+originalFilename));
    }
    if(photos.length > 0){
    for(MultipartFile photo:photos){
    if(!photo.isEmpty()){
    String originalFilename = photo.getOriginalFilename();
    photo.transferTo(new File("F:\\cache\\"+originalFilename));
    }
    }
    }
    return "main";
    }
  3. 修改相关参数

    1
    2
    3
    4
    5
    spring:  
    servlet:
    multipart:
    max-file-size: 10MB
    max-request-size: 100MB

自动配置原理

文件上传自动配置类:MultipartAutoConfiguration-MultipartProperties

  • 自动配置好了StandardServletMultipartResolver(文件上传解析器)
  • 原理步骤:
    1. 请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
    2. 参数解析器解析请求中的文件内容封装成MultipartFile
    3. 将request中文件信息封装成一个Map;MultiValueMap<String, MultipartFile>
  • FileCopyUtils实现文件流的拷贝

异常处理

默认规则

  • 默认情况下,Spring Boot提供/error处理所有错误的映射
  • 对于机器客户端,它将生成JSON响应,其中包含错误、HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个"whitelabel"错误视图,以HTML格式呈现相同的数据
  • 要对其进行自定义,添加View解析为error
  • 要完全替换默认行为,可以实现ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制,但替换其内容
  • error/下的4xx,5xx页面会被自动解析

定制错误处理逻辑

  • 自定义错误页

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

  • ErrorViewResolver实现自定义处理异常

  • 实现HandlerExceptionResolver处理异常

  • @ResponseStatus+自定义异常;底层是ResponseStatusExceptionResolver,把responsestatus注解的信息

  • Spring底层的异常,如参数转换异常

异常处理自动配置原理

  • ErrorMvcAutoConfiguration:自动配置了异常处理规则
    • 包含的Bean:DefaultErrorAttributes
      • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered
    • 包含的Bean:BasicErrorController(json+白页适配)
      • 默认处理/error请求:@RequestMapping("${server.error.path:${error.path:/error}}")
      • 页面响应new ModelAndView("error", model);
      • 容器中有组件View,id是error;(响应默认错误页)
      • 容器中放的组件BeanBeanNameViewResolver(视图解析器):按照返回的视图名作为组件id去容器中找View对象
    • 包含的Bean:DefaultErrorViewResolver
      • 如果发生错误,会以HTTP状态码作为视图页地址(viewName),找到真正的页面
      • error/405、5xx.html

如果要返回页面,就会找error视图(StaticView)。(默认是一个白页)

异常处理步骤流程

  1. 执行目标方法,目标方法运行期间有任何异常都会被catch,而且标志当前请求结束;并且用dispatchException
  2. 进入视图解析流程
  3. mv=processHandlerException;处理handler发现的异常,处理完成返回ModelAndView
    1. 遍历所有handlerExceptionResolvers,看哪个能处理当前异常(HandlerExceptionResolver处理器异常解析器)
    2. 系统默认的异常解析器
      • DefaultErrorAttributes先来处理异常,把异常信息保存到request域,并且返回null
      • 默认没有任何类能处理异常,所以异常会被抛出
        • 如果没有能处理最终底层就会发送/error请求,会被底层的BasicErrorController处理
        • 解析错误视图;遍历所有的ErrorViewResolver看哪个能解析
        • 默认的DefaultErrorViewResolver,作用是把响应状态码作为错误页的地址,error/500.html
        • 模板引擎最终响应这个页面error/500.html

Web原生组件注入(Servlet、Filter、Listener)

1. 使用Servlet APi

  • @ServletComponentScan(basePackages = "com.atguigu.admin"):指定原生Servlet组件都放在哪里

  • @WebServlet(urlPatterns = "/my"):效果:直接响应,没有经过Spring的拦截器

  • @WebFilter(urlPatterns={"/css/*","/images/*"}) :拦截指定路径的请求

  • @WebListener

DispatcherServlet如何注册?

  • 容器中自动配置了DispatcherServlet属性,绑定到WebMvcProperties;对应的配置文件配置项是spring.mvc
  • 通过ServletRegistrationBean<DispatcherServlet>把DispatcherServlet配置进来
  • 默认映射的是/路径

Tomcat处理Servlet时,如果多个Servlet都能处理到同一层路径,精确优先原则,如:

DispatcherServlet: /;MyServlet:/my,则当请求是/my时就不会通过DispatcherServlet,而是直接通过MyServlet,也就不会经过Spring流程,这是上述原生Servlet不经过拦截器的原因

2. 使用RegistrationBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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();
// return new FilterRegistrationBean(myFilter,myServlet());
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);
}
}

嵌入式Servlet容器

1. 切换嵌入式Servlet容器

  • 默认支持的webServer

    • Tomcat、Jetty、Undertow
    • ServletWebServerApplicationContext容器启动寻找ServletWebServerFactory并引导创建服务器
  • 切换服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
    <exclusion>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    </exclusion>
    </exclusions>
    </dependency>

原理:

  • SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
  • web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
  • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂---> Servlet 的web服务器
  • SpringBoot底层默认有很多的WebServer工厂;TomcatServletWebServerFactory,JettyServletWebServerFactory, orUndertowServletWebServerFactory`
  • 底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
  • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
  • ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
  • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize---this.tomcat.start();
  • 内嵌服务器,就是手动调用启动服务器的代码(tomcat核心jar包存在)

2. 定制Servlet容器

  • 实现WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>接口
    • 把配置文件的值和ServletWebServerFactory进行绑定
  • 修改配置文件server.xxx
  • 直接自定义ConfigurableServletWebServerFactory
1
2
3
4
5
6
7
8
9
10
11
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}

定制化原理

定制化常见方式

  • 修改配置文件

  • xxxxCustomizer:定制化器,可以修改xxxx的默认规则

  • 编写自定义的配置类xxxConfiguration;+@Bean替换、增加容器中默认组件;视图解析器

  • Web应用:编写一个配置类实现WebMvcConfigure即可定制化web功能;+@Bean给容器中再扩展一些组件

  • @EnableWebMvc+WebMvcConfigure—@Bean可以全面接管SpringMVC,所有规则全部自己重新配置;实现定制和扩展功能

    • 原理

    • 1、WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。配置静态资源、欢迎页等

    • 2、一旦使用 @EnableWebMvc 。就会 @Import(DelegatingWebMvcConfiguration.class)

    • 3、而DelegatingWebMvcConfiguration ,只能保证SpringMVC最基本的使用

      • 将所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
      • 自动配置了一些非常底层的组件。RequestMappingHandlerMapping,这些组件依赖的组件都是从容器中获取public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
    • 4、WebMvcAutoConfiguration 里面的配置要能生效 必须满足@ConditionalOnMissingBean(WebServerFactoryCustomizer.WebServerFactoryCustomizer)

    • 5、@EnableWebMvc 导致了 WebMvcAutoConfiguration 没有生效。

原理分析套路

场景starter->xxxxAutoConfiguration->导入xxx组件->绑定xxxProperties->绑定配置文件项

数据访问

1. SQL

HikariDataSource数据源的自动配置

导入JDBC场景
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
  • 包括:HikariPC数据源、JDBC、事务相关(spring-tx)

没有数据库驱动,为什么?

因为不确定要使用什么数据库,因此需要人为配置,默认版本是:<mysql.version>8.0.22</mysql.version>

分析自动配置
  • DataSourceAutoConfiguration:数据源的自动配置
    • 修改数据源相关的配置:spring.datasource
    • 数据库连接池的配置,当容器中没有配置DataSource时才自动配置
    • 底层配置好的连接池是:HikariDataSource
  • DataSourceTransactionManagerAutoConfiguration:事务管理器的自动配置
  • JdbcTemplateAutoConfiguration:JDBCTemplate的自动配置,可以对数据库进行CRUD
    • 可以修改@ConfigurationProperties(prefix="spring.jdbc")来修改JdbcTemplate
    • 容器中已经有了JdbcTemplate组件
  • JndiDataSourceAutoConfiguration:Jndi的自动配置
  • XADataSourceAutoConfiguration:分布式事务相关的
修改配置项
1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver

使用Druid数据源

自定义方式
  1. 创建数据源

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.17</version>
    </dependency>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
    destroy-method="close">
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <property name="maxActive" value="20" />
    <property name="initialSize" value="1" />
    <property name="maxWait" value="60000" />
    <property name="minIdle" value="1" />
    <property name="timeBetweenEvictionRunsMillis" value="60000" />
    <property name="minEvictableIdleTimeMillis" value="300000" />
    <property name="testWhileIdle" value="true" />
    <property name="testOnBorrow" value="false" />
    <property name="testOnReturn" value="false" />
    <property name="poolPreparedStatements" value="true" />
    <property name="maxOpenPreparedStatements" value="20" />
    </bean>

    或者自定义配置类

    1
    2
    3
    4
    5
    6
    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource(){
    DataSource dataSource = new DruidDataSource();
    return dataSource;
    }
  2. StatViewServlet

    StatViewServlet的用途包括:

    • 提供监控信息展示的html页面
    • 提供监控信息的JSON API

    配置Servlet

    1
    2
    3
    4
    5
    6
    @Bean
    public ServletRegistrationBean statViewServlet(){
    StatViewServlet statViewServlet = new StatViewServlet();
    ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<StatViewServlet>(statViewServlet, "/druid/*");
    return registrationBean;
    }
  3. StatFilter

    用于统计监控信息;如SQL监控、URI监控

    1
    2
    3
    4
    5
    6
    7
    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource dataSource() throws SQLException {
    DataSource dataSource = new DruidDataSource();
    dataSource.setFilters("stat"); // 加入监控功能
    return dataSource;
    }

    系统中的所有Filter:

    别名 Filter类名
    default com.alibaba.druid.filter.stat.StatFilter
    stat(监控页) com.alibaba.druid.filter.stat.StatFilter
    mergeStat com.alibaba.druid.filter.stat.MergeStatFilter
    encoding com.alibaba.druid.filter.encoding.EncodingConvertFilter
    log4j com.alibaba.druid.filter.logging.Log4jFilter
    log4j2 com.alibaba.druid.filter.logging.Log4j2Filter
    slf4j com.alibaba.druid.filter.logging.Slf4jLogFilter
    commonlogging com.alibaba.druid.filter.logging.CommonsLogFilter

    慢SQL记录配置:

    1
    2
    3
    4
    5
    6
    7
    @Bean
    public StatFilter statFilter(){
    StatFilter statFilter = new StatFilter();
    statFilter.setSlowSqlMillis(10000);
    statFilter.setLogSlowSql(true);
    return statFilter;
    }
使用官方starter方式
1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>

分析自动配置:

  • 扩展配置项spring.datasource.druid
  • DruidSpringAopConfiguration.class, 监控Spring Bean的;配置项:spring.datasource.druid.aop-patterns
  • DruidStatViewServletConfiguration.class, 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
  • DruidWebStatFilterConfiguration.class, web监控配置;spring.datasource.druid.web-stat-filter;默认开启
  • DruidFilterConfiguration.class 所有Druid自己filter的配置

配置示例:

官方说明

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

druid:
aop-patterns: com.zephont.admin.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)

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: # 对上面filters里面的stat的详细配置
slow-sql-millis: 1000 # 慢查询时间
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false

整合MyBatis

依赖:

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
1. 配置模式
  • 全局配置文件
  • SqlSessionFactory:自动配置好了
  • SqlSession:自动配置了SqlSessionTemplate组合了SqlSession
  • Mapper:只需要写的操作MyBatis的接口标注了@Mapper就会被自动扫描(@Import(AutoConfiguredMapperScannerRegistrar.class)
1
2
3
4
@EnableConfigurationProperties(MybatisProperties.class) // MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{}

1
2
@ConfigurationProperties(prefix = "mybatis") // 相关配置的对应前缀
public class MybatisProperties

配置:

  • 配置mybatis.configuration下面的配置就相当于改mybatis全局配置文件中的值
1
2
3
4
5
6
# 配置mybatis规则
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置 和下面的configuration不能共存
mapper-locations: classpath:mybatis/mapper/*.xml #sql映射文件位置
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名策略

流程:

  1. 导入mybatis官方starter
  2. 编写mapper接口,并标注@Mapper注解
  3. 编写sql映射文件并绑定mapper接口
  4. 在application.yaml中指定mapper配置文件的位置,并配置全局配置信息(mybatis.configuration)
2. 注解模式
1
2
3
4
5
@Mapper
public interface UserMapper {
@Select("select * from tb_user where id=#{id}")
public User getById(Long id);
}
3. 混合模式
1
2
3
4
5
6
@Mapper
public interface UserMapper {
@Select("select * from tb_user where id=#{id}")
public User getById(Long id);
public void insert(User user);
}

流程:

  1. 导入mybatis官方starter
  2. 配置application.yaml,指定mapper-location
  3. 编写Mapper接口并标注@Mapper注解
  4. 简单方法直接注解方式
  5. 复杂方法编写mapper.xml进行绑定映射
  6. @MapperScan("com.zephon.admin.mapper")简化,其它接口就不用标注@Mapper注解

整合MyBatis-Plus

  1. 导入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
    </dependency>
  2. 继承接口/类

    1
    2
    3
    4
    5
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {


    }
    1
    2
    3
    public interface UserService extends IService<User> {

    }

2. NoSQL

Redis自动配置
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

自动配置:

  • RedisAutoConfiguration自动配置类。RedisProperties属性类 --> spring.redis.xxx是对redis的配置
  • 连接工厂是准备好的。LettuceConnectionConfigurationJedisConnectionConfiguration
  • 自动注入了RedisTemplate<Object, Object>:xxxTemplate
  • 自动注入了StringRedisTemplate,k:v都是String
  • 底层只要使用StringRedisTemplateRedisTemplate就可以操作Redis

例:

1
2
3
4
5
6
7
8
9
10
11
@Autowired
StringRedisTemplate redisTemplate;
@Test
void testRedis(){
ValueOperations<String, String> operations = redisTemplate.opsForValue();

operations.set("hello","world");

String hello = operations.get("hello");
System.out.println(hello);
}

切换到jedis客户端

1
2
3
4
5
6
7
8
9
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

配置:

1
2
3
4
5
6
7
8
9
spring:
redis:
host: xxx
port: 6379
password: 123456
client-type: jedis
jedis:
pool:
max-active: 10

JUnit5单元测试

Junit5的变化

JUnit5=Junit Platform + JUnit Jupiter + Junit Vintage

JUnit Platform:是在JVM上启动测试框架的基础,不仅支持JUnit自制的测试引擎,其它测试引擎也都可以接入

JUnit Jupiter:提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于JUnit Platform上运行

JUnit Vintage:由于JUnit已经发展多年,为了照顾老项目,JUnit Vintage提供了兼容JUnit4.x,JUnit3.x的测试引擎

注:SpringBoot2.4以上版本移除了默认对Vintage的依赖,如果需要兼容JUnit4需要自行引入,否则不能使用JUnit4的功能@Test

对应依赖

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

使用方法:

1
2
3
4
5
6
7
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Test
void testMethod() {

}
}

JUnit5常用注解

  • @Test:表示方法是测试方法
  • @ParameterizedTest:表示方法是参数化测试
  • @RepeatedTest:表示方法可重复执行
  • @DisplayName:为测试类或者测试方法设置展示名称
  • @BeforeEach:表示在每个单元测试之前执行
  • @AfterEach:表示在每个单元测试之后执行
  • @BeforeAll:表示在所有单元测试之前执行
  • @AfterAll:表示在所有单元测试之后执行
  • @Tag:表示单元测试类别
  • @Disabled:表示测试类或测试方法不执行
  • @Timeout:表示测试方法运行如果超过了指定时间将会返回错误
  • @ExtendWith:为测试类或测试方法提供扩展类引用
  • 更多

断言(assertions)

1. 简单断言

用来对单个值进行简单的验证。如:

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);

assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);

assertFalse(1 > 2);
assertTrue(1 < 2);

assertNull(null);
assertNotNull(new Object());
}

2. 数组断言

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

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

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)
);
}

4. 异常断言

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

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

}

5. 超时断言

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

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

6. 快速失败

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

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

前置条件(assumptions)

JUnit5中的前置条件(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")
);
}
}
  • 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());
}
}
}
}

参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得不同的参数多次运行测试成为可能,也为单元测试提供很多便利。

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

  • @ValueSource:为参数化测试指定入参来源,支持八大基础类和String类型、Class类型
  • @NullSource:表示为参数化测试提供一个null的入参
  • @EnumSource:表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定csv文件内容作为参数化测试入参
  • MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

此外,还可以支持外部的各类入参如CSV、YML、JSON文件甚至方法的返回值也可以作为入参。只需要实现ArgumentsProvider接口,任何外部文件都可以作为它的入参。

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");
}

JUnit4到JUnit5迁移指南

在进行迁移的时候需要注意如下的变化:

  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
  • 把@Before 和@After 替换成@BeforeEach 和@AfterEach。
  • 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。
  • 把@Ignore 替换成@Disabled。
  • 把@Category 替换成@Tag。
  • 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。

生产指标监控

SpringBoot Actuator

1、简介

每个微服务在云上部署之后,都需要对其进行监控、追踪、审计和控制等。SpringBoot抽取了Actuator场景,使得每个微服务快速引用即可获得生产级别的应用监控、审计等功能。

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

2、如何使用

  1. 引入场景

  2. 访问http://localhost:8080/actuator/**

  3. 暴露所有监控信息为HTTP

    1
    2
    3
    4
    5
    6
    management:
    endpoints:
    enabled-by-default: true #暴露所有端点信息
    web:
    exposure:
    include: '*' #以web方式暴露
  4. 测试

    http://localhost:8080/actuator/beans

    http://localhost:8080/actuator/configprops

    http://localhost:8080/actuator/metrics

    http://localhost:8080/actuator/metrics/jvm.gc.pause

    http://localhost:8080/actuator/endpointName/detailPath

    ......

3、可视化

https://github.com/codecentric/spring-boot-admin

Actuator Endpoint

1、常用端点

ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
mappings 显示所有@RequestMapping路径列表。
scheduledtasks 显示应用程序中的计划任务。
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown 使应用程序正常关闭。默认禁用。
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump 执行线程转储。
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile 返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

最常用的Endpoint:

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

2、Health Endpoint

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

  • Health Endpoint返回的结果是一系列健康检查后的一个汇总报告
  • 很多的健康检查默认是自动配置好了的,如:数据库、Redis等
  • 可以很容易的添加自定义的健康检查机制

3、Metrics Endpoint

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

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

4、管理Endpoints

  1. 开户与禁用Endpoints

    • 默认所有Endpoint除了shutdown都是开启的

    • 需要开启或禁用某个Endpoint,配置模式为management.endpoint.<endpointName>.enabled=true

      1
      2
      3
      4
      management:
      endpoint:
      beans:
      enabled: true
    • 或禁用所有的Endpoint然后手动开启指定的Endpoint

      1
      2
      3
      4
      5
      6
      7
      8
      management:
      endpoints:
      enabled-by-default: false
      endpoint:
      beans:
      enabled: true
      health:
      enabled: true
  2. 暴露Endpoints

    支持的暴露方式:

    • HTTP:默认只暴露health和info Endpoint

    • JMX:默认暴露所有Endpoint

    • 除了Health和info,剩下的Endpoint都应该进行保护访问,如果引入SpringSecurity,则会默认配置安全访问规则

      ID JMX Web
      auditevents Yes No
      beans Yes No
      caches Yes No
      conditions Yes No
      configprops Yes No
      env Yes No
      flyway Yes No
      health Yes Yes
      heapdump N/A No
      httptrace Yes No
      info Yes Yes
      integrationgraph Yes No
      jolokia N/A No
      logfile N/A No
      loggers Yes No
      liquibase Yes No
      metrics Yes No
      mappings Yes No
      prometheus N/A No
      scheduledtasks Yes No
      sessions Yes No
      shutdown Yes No
      startup Yes No
      threaddump Yes No

定制Endpoint

1、定制Health信息

实现HealthIndicator接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class MyHealthIndicator implements HealthIndicator {

@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}

}

构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
1
2
3
4
5
6
# management.endpoint.端点名.xxxx:对某个端点的具体配置
management:
endpoint:
health:
enabled: true # 开放端点
show-details: always #总是显示详细信息。可显示每个模块的状态信息

或继承抽象类AbstractHealthIndicator

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
@Component
// 对应的名称就是myCom
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);

}
}

2、定制info信息

常用两种方式:

  1. 编写配置文件

    1
    2
    3
    4
    5
    info:
    appName: boot-admin # 自定义key: value
    version: 2.0.1
    mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
    mavenProjectVersion: @project.version@
  2. 编写InfoContributor

    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"));
    }

    }

3、定制Metrics信息

  1. SpringBoot支持自动适配的Metrics

  2. 定制Metrics

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class MyService{
    Counter counter;
    public MyService(MeterRegistry meterRegistry){
    counter = meterRegistry.counter("myservice.method.running.counter"); // 指标注册
    }

    public void hello() {
    counter.increment();
    }
    }


    //也可以使用下面的方式
    @Bean
    MeterBinder queueSize(Queue queue) {
    return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
    }

4、定制Endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Endpoint(id = "container")
public class DockerEndpoint {


@ReadOperation
public Map getDockerInfo(){
// 端点读操作,即访问对应URL时返回数据
return Collections.singletonMap("info","docker started...");
}

@WriteOperation
private void restartDocker(){
// JMX可以执行的操作
System.out.println("docker restarted....");
}

}

原理解析

Profile功能

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

1、application-profile功能

  • 默认配置文件appliction.yaml任何时候都会加载
  • 指定环境配置文件application-{env}.yaml
  • 激活指定环境
    • 配置文件激活:spring.profiles.active=test
    • 命令行激活:--spring.profiles.active=test
      • 可以进一步修改其中的配置--spring.profiles.active=test --server.port=9000
  • 默认配置与环境配置同时生效
  • 同名配置项,profile配置优先

2、@Profile条件装配功能

1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@Profile("test") // 表示测试环境才生效
public class ProductionConfiguration {

// ...

}

3、profile分组

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

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

外部化配置

https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config

1、外部配置源

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

2、配置文件查找位置

  • classpath根路径
  • classpath根路径下config目录
  • jar包当前目录
  • jar包当前目录的config目录
  • /config子目录的直接子目录

优先级自下往上

3、配置文件加载顺序

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

总结:

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

自定义starter

1、starter启动原理

  • starter-pom引入自定义的autoconfigure包
  • autoconfigure包配置使用META-INF/spring.factoriesEnableAutoConfiguration的值,使得项目启动加载指定的自动配置类
  • 编写自动配置类xxxAutoConfiguration -> xxxProperties
    • @Configuration
    • @Conditional
    • @EnableConfigurationProperties
    • @Bean
    • ......

引入starter ---> xxxAutoconfiguration ---> 容器中放入组件 ----> 绑定xxxProperties ---> 配置项

2、自定义starter

hello-spring-boot-starter(启动器):在pom.xml中引入自定义的自动配置包

在类路径创建META-INF/spring.factories文件,配置:

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.abc.hello.auto.HelloServiceAutoConfiguration

hello-spring-boot-starter-autoconfigure(自动配置包)

  • hello.bean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @ConfigurationProperties("abc.hello")
    public class HelloProperties {
    private String prefix;
    private String suffix;

    public String getPrefix() {
    return prefix;
    }

    public void setPrefix(String prefix) {
    this.prefix = prefix;
    }

    public String getSuffix() {
    return suffix;
    }

    public void setSuffix(String suffix) {
    this.suffix = suffix;
    }
    }
  • hello.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 默认不要放在容器中
    */
    public class HelloService {
    @Autowired
    HelloProperties helloProperties;

    public String sayHell(String username) {
    return helloProperties.getPrefix() + ":" + username + "->" + helloProperties.getSuffix();
    }
    }
  • hello.auto.HelloServiceAUtoConfiguration

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    @EnableConfigurationProperties(HelloProperties.class) // 默认HelloProperties放在容器中
    public class HelloServiceAutoConfiguration {
    @ConditionalOnMissingBean(HelloService.class)
    @Bean
    public HelloService helloService(){
    return new HelloService();
    }
    }

SpringBoot原理

SpringBoot启动过程

  1. 创建SpringApplication
    • 保存一些信息
    • 判断当前应用的类型(Servlet)。ClassUtils
    • bootstrappers:初始启动引导器(List<Bootstrapper>):去spring.factories文件中找org.springframework.boot.Bootstrapper
    • ApplicationContextInitializer,去spring.factoriesApplicationContextInitializer
      • List<ApplicationContextInitializer<?> initializers
    • ApplicationListener应用监听器,去spring.factoriesApplicationListener
      • List<ApplicationListener<?>> listeners
  2. 运行SpringApplication
    • StopWatch
    • 记录应用的启动时间
    • 创建引导上下文(Context环境)createBootstrapContext()
      • 获取到之前所有的bootstrappers,逐个执行initialize()来完成对引导启动器上下文环境设置
    • 让当前应用进入headless模式,java.awt.headless
    • 获取所有RunListener(运行时监听器) (为了方便所有Listener进行事件感知)
      • getSpringFactoriesInstances:去spring.factoriesSpringApplicationRunListener
      • 遍历SpringApplicationRunListener调用starting方法
      • 这个过程相当于通知所有感兴趣系统正在启动过程的监听器
    • 保存命令行参数:ApplicationArguments
    • 准备环境:prepareEnvironment()
      • 返回或创建基础环境信息对象,StandardServletEnvironment
      • 配置环境信息对象
        • 读取所有的配置源的配置属性值
      • 绑定环境信息
      • 监听器调用listener.enviromentPrepared()通知所有监听器当前环境准备完成
    • 创建IOC容器(createApplicationContext())
      • 根据项目类型(Servlet)创建容器
      • 当前会创建AnnotationConfigServletWebServerApplicationContext
    • 准备IOC容器(ApplicationContext )的基本信息,prepareContext()
      • 保存环境信息
      • IOC容器的后置处理流程
      • 应用初始化器(applyInitializers)
        • 遍历所有ApplicationContextInitializer,调用initialize,来对ioc容器进行初始化扩展功能
        • 遍历所有listener调用contextPrepared,(EventPublishRunListener);通知所有的监听器contextPrepared完成
      • 所有的listener调用contextLoaded,通知所有的监听器contextLoaded完成
    • 刷新IOC容器(refreshContext)
      • 创建容器中的所有组件(Spring注解)
    • 容器刷新完成后工作 afterRefresh
    • 所有监听器调用listeners.started(context)通知所有的监听器started
    • 调用所有runners;callRunners()
      • 获取容器中的ApplicationRunner
      • 获取容器中的CommandLineRunner
      • 合并所有runner并且按照@Order进行排序
      • 遍历所有runner,调用run方法
    • 如果以上有异常则调用listenerfailed
    • 调用所有监听器的running方法listeners.running(context)通知所有的监听器running
    • running如果有问题,继续通知failed,调用所有Listener的failed,通知所有监听器failed