0%

SpringBoot

微服务阶段

Springboot:jar包,内嵌tomcat

最大特点:自动装配

image-20210817174815920

Springboot原理简析

创建项目

  1. 从官网Spring Initializr进行配置并下载压缩文件
  2. idea直接集成,实现原理同上

Springboot新功能

  • 自动配置好SpringMVC

    • 引入SpringMVC全套组件
    • 自动配好SpringMVC常用组件(功能)
  • 自动配号Web常见功能:如字符集编码问题

  • 默认包结构

    • 主程序所在包及以下的所有子包内的组件会被默认扫描出来

    • 无需手动配置包路径

    • 如果要在上层目录也进行扫描,则可以配置注解属性

      @SpringBootApplication(scanBasePackages = "com.lan5th")

  • 各种配置拥有默认值

    • 默认配置最终映射到MultipartProperties
    • 配置文件的值最终绑定到每个类上,并在容器中创建对象
  • 按需加载所有自动配置项

    • starter启动器,引入哪些启动器就启动哪些场景的依赖
    • springboot所有自动配置功能都在spring-boot-autoconfigure包中

自动装配原理

  • @SpringBootApplication标识这个类是springboot的应用

    • springboot项目启动时会从spring-boot-autoconfigure-2.x.x.jar\META-INF\spring.factories下自动获取指定的值
    • 它会把所有需要导入的组件一类名的方式返回,以添加到springboot容器中
    • 这个文件中存在大量xxxAutoConfigiration的类名,提供了当前starter所需的所有组件
  • SpringApplication.run()

    运行过程:

    • 推断应用类型(是否为web项目)
    • 查找并加载所有可用初始化器,设置到initializers属性中
    • 找出所有应用程序监听器,添加到listeners属性中
    • 团短并设置main方法的定义类,找到运行主类

简单案例

spring.factories中配置以下自动配置类HttpEncodingAutoConfiguration.java,我们以它为例更进一步了解原理

HttpEncodingAutoConfiguration.java

1
2
3
4
5
6
7
8
9
//标识是一个配置类
@Configuration(proxyBeanMethods = false)
//自动装配属性ServerProperties
@EnableConfigurationProperties(ServerProperties.class)

@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {...}
  • ServerProperties.java

    1
    2
    3
    //这与我们在配置文件中所标写的前缀所绑定,并在实例化时进行自动注入
    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
    public class ServerProperties{...}
  • ConditionalOn注解

    image-20210821182716543

HttpEncodingAutoConfiguration开启自动装配,并给ServerProperties一些属性赋默认值,ServerProperties从yml配置文件中读取前缀相关的属性,替换默认值,从而加载相关组件。

总结

  • springboot先加载所有的自动动配置类xxxAutoConfiguration
  • 每个自动配置类按照条件进行生效,默认绑定配置文件所指定的值,通过xxxProperties与配置文件进行绑定
  • 生效的配置类为容器中注入相应的组建环境
  • 用户如果自己进行了某些组建的配置,就以用户的配置为优先
  • 自定义配置方法
    • 用户自定义@Bean进行注入,替换底层代码
    • 用户查看组件绑定的配置文件前缀进行修改

请求映射原理

DispatcherServlet.java中由doDispatch()方法进行请求处理,并调用getHandler()方法进行处理器映射器的选择

1
2
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
1
2
3
4
5
6
7
8
9
10
11
12
13
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//getHandler方法会在所有能够得到的handlerMapping中进行遍历并试图进行url匹配
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

image-20210910204412853

其中,RequestMappingHandlerMapping为SpringBoot帮我们配置的映射器,在mappingRegistry属性中注册了我们所有在Controller中配置的请求url和执行方法的匹配映射

image-20210910204619014

同理,WelcomePageHandlerMapping会在RequestMappingHandlerMapping匹配url失败后进行匹配,并且仅匹配/路径,用于web应用首页的跳转

参数处理原理

参数处理流程

  1. 处理器适配器HandlerAdapter

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

    image-20210912100135032

  2. 执行目标方法

    1
    2
    3
    //DispatcherServlet
    // Actually invoke the handler.
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    1
    2
    3
    //RequestMappingHandlerAdapter(具体的handlerAdapter)
    //执行目标方法
    mav = invokeHandlerMethod(request, response, handlerMethod);
  3. 参数解析器ArgumentResolver

    目标方法能够写的参数类型都取决于参数解析器

    image-20210912101130177

  4. 返回值处理器ReturnValueHandler

    目标方法能够写的返回值类型都取决于返回值处理器

    image-20210912101408958

复杂参数

image-20210913105920294

给Map或Model对象里面添加数据相当于给HttpServletRequest中setAttribute。

Map和Model在实际解析参数时都会返回mavContainer.getModel();其中的BindingAwareModelMap能够同时实现Map和Model的具体功能

在执行具体方法时如果同时传入Map和Model对象,实际运行时只有同一个BindingAwareModelMap对象

目标方法执行完成时将所有的数据都放在ModelAndViewContainer(mavContainer)中,包含目标视图VIew和相关数据Model

ModelAndView转移流程

  1. Map和Model被BindingAwareModelMap实现
  2. BindingAwareModelMap被封装于ModelAndViewContainer
  3. BindingAwareModelMap被取出分封装于ModelAndView
  4. BindingAwareModelMap数据被取出封装于ModelMap(新对象)中
  5. exposeModelAsRequestAttributes(model, request);暴露模型作为请求域属性
  6. 对ModelMap进行遍历并将其添加到请求域参数中

自定义对象参数

由ServletModelAttributeMethodProcessor参数处理器(有重名)进行解析

  1. 根据实体类判断是否为简单类型

  2. 创建出一个对象实例JavaBean

  3. WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name)Web数据绑定器,可以将请求的参数封装在指定的JavaBean(方法中指attribute)中

    WebDataBinder底层有非常多的数据转换器Converters来帮助我们转换常见的参数类型

    image-20210913114942990

  4. GenericConversionService在设置每一个值时,会遍历所有Converter,寻找能将转换指定数据类型的转换器,转换器底层利用反射获取实体类数据类型并进行相应转换

  5. 我们也可以自定义Converter:实现WebMvcConfigurer中的addFormatters方法,用于添加我们自己的Converter

    上层接口

    image-20210913165406673

数据响应原理

返回参数

springboot已经帮我们导入了jackson的相关依赖

returnValueHandler返回值处理器对不同的返回值类型进行处理,其上层接口结构:

image-20210913164852244

image-20210913162803918

执行流程:

  • supportsReturnType判断是否支持该类型返回值

  • handleReturnType进行处理

  • 利用MessageConverters处理返回值

    • 内容协商:浏览器默认会以请求头的方式高速服务能够就接受怎样的内容类型

    • 服务器根据自身能力决定能生产出什么样类型的内容数据

    • 遍历所有容器底层的HttpMessageConverter,得到MappingJackson2HttpMessageConverter可以将对象转为Json格式

      image-20210913192101770

      image-20210913192255767

内容协商

流程:

  • 判断当前响应头中是否已经有了确定的媒体类型

  • 获取客户端中支持的内容类型(请求头accept字段,如application/json,一般都带有权重)

  • 遍历循环所有的MessageConverter,寻找支持操作对象的Converter列表

    image-20210913200339922

  • 将Converter支持的媒体类型统计出来

    客户端需要application/json,服务端可以处理的类型列表:

    image-20210913200226642

  • 进行内容协商的最佳匹配

    1
    2
    3
    4
    5
    6
    7
    for (MediaType requestedType : acceptableTypes) {
    for (MediaType producibleType : producibleTypes) {
    if (requestedType.isCompatibleWith(producibleType)) {
    mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
    }
    }
    }
  • 用支持将对象转为最佳匹配媒体类型的converter进行内容转化

开启参数方式的内容协商

配置文件

1
spring.mvc.contentnegotiation.favor-parameter=true

使用方式url?format=相应格式,如http://localhost:8080/test/user?format=xml

进行自定义Convertor实现HttpMessageConverter<支持操作的数据类型>接口,并在WebMvcConfigurer中添加进容器

image-20210915231645160

或自定义协议

image-20210915231819843

模板引擎与视图解析原理

  1. 目标方法处理的过程中,所有数据都会被放在ModelAndViewContainer中,包括数据和试图抵制

  2. 方法的参数是自定义对象时(从请求参数中获取),也会将其放在ModelAndViewContainer中

  3. 任何目标方法执行完成后都会返回ModelAndView对象

  4. processDispatcherResult处理派发结果(页面响应方式)

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

      • 根据方法的String返回值得到View对象(定义了页面的渲染逻辑)

      • 所有的视图解析器尝试是否能通过当前返回值得到View对象

        如返回值redirect:/main.html由ThymeleafViewResolver进行相关判断并new了一个RedirectView对象

        image-20210916223209162

      • ContentNegotiatingViewResolver包含了下面的所有视图解析器,原理还是利用这些视图解析器获取View对象

      • VIew视图对象调用自身的render方法对页面进行渲染

View视图对象如何进行页面渲染?

  • 返回值以forward开始:new InternalResourceView(forwardUrl)->

    request.getRequestDispatcher(path).forward(request, response)

  • 返回值以redirect开始:new RedirectView()->

    response.sendRedirect(encodedURL)

  • 返回值是普通字符串:new ThymeleafView()->

    ThymeleafView调用自己底层的输出流方法对页面进行渲染

拦截器原理

  1. 根据前当前请求找到可以处理请求的handler和handler的所有拦截器

    image-20210917132912615

    • 顺序执行所有拦截器的preHandle方法
    • 如果有某个拦截器返回为false,则倒序执行所有已经执行了的拦截器的afterCompletion
  2. 如果任何一个拦截器返回为false,则会跳出不执行目标方法

    • 如果所有拦截器都返回为true,则执行目标方法
    • 倒序执行所有拦截器的postHandle方法
  3. 页面成功渲染之后也会倒序触发afterCompletion方法

  4. 以上任何一步出现异常也会直接倒序触发afterCompletion方法

image-20210917134058094

文件上传原理

文件上传自动配置类MultipartAutoConfiguration自动配置好了StandardServletMultipartResolver文件上传解析器

  1. 文件上传解析器判断是否为Multipart请求,并封装为MultipartHttpServletRequest文件上传请求
  2. 参数解析器来解析请求中的文件内容封装成MultipartFIle
  3. 将request中的文件信息封装为Map:MultiValueMap<String, MultipartFile>
  4. FileCopyUtils可以实现文件流的拷贝

错误处理原理

异常处理自动配置类ErrorMvcAutoConfiguration

  • 异常处理自动配置原理

    image-20210917160830225

    如果想要返回页面,就会找到error视图(staticView),默认为空白页

  • 异常处理流程

    1. 目标方法运行期间任何异常,都会被catch,并被dispatchException封装

    2. 进入视图解析流程(页面渲染)

      processDispatchResult(processedRequest, response, mappedHandler, mv, dispathcerException)

    3. processHandlerException方法处理handler发生的异常,处理完成返回ModelAndView

      • 遍历所有的handlerExceptionResolvers,看谁能处理当前异常

        image-20210917162126075

      • 系统默认的异常解析器

        image-20210917161933384

        • defaultErrorAttribute现来处理异常,把异常信息保存到request域,并且返回null
        • 默认没有任何handlerExceptionResolvers能够处理,因此直接抛出异常
          • 如果这种情况,则spring会自动发送一个/error请求,会被底层的BasicErrorController处理
          • 解析错误视图,遍历所有的ErrorViewResolver看谁能够解析
          • 默认DefaultErrorViewResolver作用是把响应状态码作为错误页的地址,如error/500.html
          • 模板引擎最终响应这个页面
  • 定制错误处理逻辑

    • 在/error路径下添加404.html,5xx.html错误页

    • @ControllerAdvice+@ExceptionHandler处理全局异常:底层由ExceptionHandlerExceptionResolver支持

      1
      2
      3
      4
      5
      6
      7
      8
      @ControllerAdvice
      public class GlobalExceptionHandler {
      //处理计算错误异常
      @ExceptionHandler({ArithmeticException.class,NullPointerException.class})
      public String handleArithException(Exception e){
      return "login";
      }
      }
    • ResponseStatus+自定义异常:底层由ResponseStatusExceptionResolver支持,把responsestatus注解的信息调用response.sendError(statusCode,resolvedReason)给Tomcat发送/error请求

      1
      2
      3
      4
      5
      @ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "用户数量太多了")
      public class UserTooManyException extends RuntimeException{
      public UserTooManyException(){}
      public UserTooManyException(String msg){super(msg);}
      }
    • DefaultHandlerExceptionResolver为Spring处理框架底层的异常

      response.sendError(HttpServletResponse.SC_BAD_REQUEST,ex.getMessage)

      自定义实现HandlerExceptionResolver处理异常,可以作为全局磨人的异常处理规则

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      //优先级设置为最高,否则会由其他三个HandlerExceptionResolver接管异常的处理
      @Order(value = Ordered.HIGHEST_PRECEDENCE)
      @Component
      public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
      @Override
      public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
      try {
      response.sendError(511,"自定义的错误类型");
      } catch (IOException e){
      e.printStackTrace();
      }
      //尽管我们未对ModelAndView作任何处理,但只要返回值不为空
      //都会直接跳出循环,不再进行HandlerExceptionResolver的遍历
      return new ModelAndView();
      }
      }
  • ErrorViewResolver

    • response.sendError,error请求就会转给Controller
    • 当一个异常没有任何handlerExceptionResolvers能够处理,error请求也会转给Controller
    • basicErrorController要去的页面地址是ErrorViewResolver解析的

嵌入式Servlet容器

image-20210917205230494

切换容器类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

自定义Servlet容器

  • 修改配置文件server.xxx
  • 注入ConfigurableServletWebServerFactory
  • 实现WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>接口

定制化原理

定制化的常见方式

  • 修改配置文件

  • xxxCustomizer

  • 编写自定义配置类 xxxConfig + @Bean增加组件来替换默认配置

  • Web应用实现WebMvcConfigurer定制化Web功能

  • WebMvcConfigurer+@EnableWebMvc:全面接管Mvc,自动配置全部失效,所有配置项都需要我们进行配置

    image-20210917211317115

原理分析常用方式

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

SpringBoot配置

Maven in SpringBoot

pom.xml中规定了springboot项目的府项目,而在父项目中规定了许多常用依赖的版本号,我们进行依赖导入时一般不需要再手动指定版本号,减少了版本不兼容的发生频率

在进行依赖导入时通常直接导入启动器,一个启动器包含了多个相关的maven依赖,使用起来更为方便,一般格式为

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

一般来说,spring-boot-starter-XXX格式为springboot官方启动器,不需要指定版本,而XXX-spring-boot-starter为第三方启动器,版本管理文件中是否有而决定是否需要手动配置

所有场景启动器有一个共同的依赖spring-boot-starter

1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.3</version>
<scope>compile</scope>
</dependency>

yaml

springboot配置文件application.yml可以后缀使用.yml .yaml .properties(yml是yaml的缩写)

其中yml作为springboot的推荐配置文件类型,可以存储多种数据类型

yaml还可以获取pom.xml配置文件中的信息,如:

1
2
info:
appVersion: @project.version@
1
2
3
4
5
6
7
8
9
10
11
#key-value键值对
name: lan5th
#对象
student:
name: lan5th
age: 3
#数组
pets:
- cat
- dog
- pig

并且yml可以注入到我们的配置之中

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
//使用此注解容器会将yml中指定前缀的属性自动注入
@ConfigurationProperties(prefix = "person")
public class User {
private String name;
private Integer age;
private Boolean married;
private Date birth;
private Map<String,Object> map;
private List<Object> list;
private Dog dog;
}

Dog.java

1
2
3
4
5
6
7
8
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Dog {
private String name;
private Integer age;
}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
person:
name: lan5th
age: 3
married: false
birth: 2021/8/19
map: {key1: value1, key2: value2}
list:
- c
- java
- python
dog:
name: myDog
age: 3

注意:yaml的字符串转义问题

1
2
myString: 'string1 /n string2'
newString: "string1 /n string2"
  • 单引号会输出string1 /n string2原字符串

  • 双引号会进行转义,变为

    1
    2
    string1 
    string2

yml与properties功能对比

image-20210819225635969

  • 松散绑定:yml文件中-后的字母默认大写,如yml中的last-name注入式会变成lastName

  • SpEL:使用properties文件时用于给属性赋值

    1
    2
    @Value("${name}")
    private String name;
  • JSR303数据校验

    需要导入starter环境

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

    使用方式如下,可以只接受email格式的注入

    1
    2
    3
    4
    5
    @Validated
    public class User {
    @Email
    private String email;
    }

    部分校验功能

    image-20210819231649005

  • 复杂类型封装:如对象

共有四处可以配置application.yml配置文件

按优先级从高到低排序如下:

  • file:./config/:项目根路径下的config文件夹
  • file:./:项目根路径下直接创建文件
  • classpath:/congfig/:resources目录下的config文件夹
  • classpath:/:resources目录下直接创建文件(idea创建springboot项目默认于此)

yaml提示依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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>

多环境配置文件

新建多套配置文件

application.properties

application-dev.properties生产环境

application-test.properties测试环境

  • 通过配置文件激活多环境(properties或yaml)

    1
    2
    #直接指定后缀可直接切换
    spring.profiles.active=dev
  • 在部署时命令行激活(命令行最为优先)

    1
    java -jar springboot-0.0.1-SNAPSHOT.jar --spring.profiles.active=test
  • yml单文件激活环境

    单文件多环境

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    server:
    port: 8080
    spring:
    profiles:
    active: dev
    #---用于分割不同的生产环境
    ---
    server:
    port: 8081
    spring:
    profiles: dev
    ---
    server:
    port: 8082
    spring:
    profiles: test
  • 自定义类按环境切换@Profile可以标注在类上和方法上

    1
    2
    3
    4
    5
    6
    @Data
    @Profile("dev") //只有dev环境才会启用
    public class User{
    private String name;
    private String password;
    }
    1
    2
    3
    @Configuration
    @Profile("test") //只有test环境才会启用
    public class MyConfig{...}
  • 指定激活多个配置文件

    现在有四个配置文件

    application.properties

    application-dev.properties

    application-prod.properties

    application-test.properties

    1
    2
    3
    4
    5
    6
    spring.profiles.active=myenv
    # 自定义环境myenv
    spring.profile.group.myenv[0]=dev
    spring.profile.group.myenv[1]=prod
    # 自定义环境mytest
    spring.profile.group.mytest[0]=test

    当激活myenv时,application.properties, application-dev.properties, application-prod.properties会全部生效,实现多个配置文件的加载

高级自定义配置

1
2
3
4
5
6
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {

}
}

通过向容器中注入WebMvcConfigurer来实现一些自定义的组件,这个接口有许多添加组件的默认方法,借由此可以进行添加

配置加载规则

  • 常见外部配置源

    java属性文件,yaml,环境变量,命令行参数

  • 配置文件加载优先级

    image-20210918214141412

    从上至下优先级依次变高

  • 总结:指定环境优先,外部优先,后项覆盖前项同名项

Web开发

常用注解

  • @Configuration标识是一个配置类

    @Bean,方法级别的注解,代替xml向容器中添加组件,组件的id默认为方法名

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class Myconfig {
    @Bean
    public User userRegist(){
    return new User("lan5th","123456");
    }
    }
    • 配置类本身也是组件
    • @Configuration注解中的属性proxyBeanMethods默认为true,表示配置类在容器中以代理对象形式所存在,只能拿到单例的Bean实例。同时以此区分Full模式与Lite模式
    1
    2
    3
    4
    5
    6
    7
    @Bean
    @ConditionalOnBean(MultipartResolver.class)
    @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
    public MultipartResolver multipartResolver(MultipartResolver resolver) {
    // Detect if the user has created a MultipartResolver but named it incorrectly
    return resolver;
    }
    • 这个方法中添有@Bean注解,而且有传入值,表示执行方法时会从容器中自动寻找一个与MultipartResolver类型相匹配的组件,将它的属性赋值给新的multipartResolver
  • @Import必须放置在组件类型的注解上:@Component,@Configuration,@Controller,@Service,@Repository

    用于给容器中导入自己所需要的组件或第三方组件,如

    1
    2
    3
    @Import(DBHelper.class)
    @Configuration
    public class Myconfig{...}
  • @Conditional满足指定条件时再进行注入

    1
    2
    3
    4
    5
    6
    //常见注解
    @ConditionalOnBean//指定Bean在容器中存在
    @ConditionalOnMissingBean//指定Bean在容器中不存在
    @ConditionalOnClass//指定Class在容器中存在
    @ConditionalOnResource//项目路径中存在指定资源
    @ConditionalOnWebApplication//当前运行环境为Web环境
  • @ImportResource在某个配置类上导入xml类型的Bean资源,如

    @ImportResource(classpath:beans.xml)

  • @ConfigurationProperties必须配合组件类型的注解使用,将配置文件中指定前缀的kv自动注入实体类中的属性。,如

    @ConfigurationProperties(prefix = "user")

    或在配置类中使用@EnableConfigurationProperties(User.class)来代替@Component + @ConfigurationProperties的组合

Controller请求参数

注解参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@PathVariable("id") //Restful获取url参数
@PathVariable Map<String,String> pv //Restful获取url参数并自动封装,只能Map<String,String>类型
@RequestHeader("User-Agent") //获取请求头
@RequestHeader Map<String,String> header //获取请求头并自动封装,只能Map<String,String>类型
@RequestParam("id") //获取普通类型url参数
@RequestParam Map<String,String> params //获取普通类型url参数并自动封装,只能Map<String,String>类型
@CookieValue("id") //获取cookie参数
@CookieValue Cookie cookie //获取cookie并封装成对象

@RequestBody //获取post表单数据
@RequestAttribute("msg") //获取request.setAttribute中设置的参数(用于方法之间)
@ModelAttribute

@MatrixVariable

导入静态资源

WebMvcProperties.java中定义了

1
private String staticPathPattern = "/**";

WebProperties.java中定义了

1
2
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };

这4处即为一般默认能够引入静态资源的位置(classpath:/META-INF/resources/即为WebJars下的相关路径)

访问方式

  • webjars localhost:8080/webjars/+fileName
  • public, static, resources localhost:8080/+fileName

优先级:resources>static(默认)>public

可以通过在配置文件中配置spring.mvc.static-path-pattern来更改静态资源访问url前缀,配置spring.resources.static-locations来更改静态资源存储位置

原理探究

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//如果一个类只有一个有参构造器,那么所有的参数都会自动从容器中匹配获取
public WebMvcAutoConfigurationAdapter(
org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,
ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties
: webProperties.getResources();
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}

模板引擎Thymeleaf

代替了之前学习的jsp,允许在html页面中写入逻辑语法

直接导入相应启动器

1
2
3
4
5
6
7
8
9
10
<!--Thymeleaf本体-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--添加对java8时间LocalDate和LocalDateTime的支持依赖-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

ThymeleafProperties.java中规定了Thymeleaf保存文件的位置和后缀

1
2
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";

使用时需要导入命名空间xmlns:th="http://www.thymeleaf.org"

基础语法

  • 表达式

    image-20210901213203012

  • 取值方法

    image-20210830195948186

扩展装配SpringMVC

自定义视图解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
//不能添加@EnableWebMvc,否则自动配置将会全部失效,需要手动进行全部配置
public class MyConfig {
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}

public static class MyViewResolver implements ViewResolver{
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return null;
}
}
}

注册到Spring容器之后,我们就可以使用我们自定义的视图解析器相关配置

国际化

  • 配置i18n文件夹(internationalization的缩写)

    其中保存.properties文件并以键值对的形式保存不同翻译

    语言切换,例:

    • 中文login_zh_CN.properties
    • 英文login_en_US.properties
  • 如果需要进行按钮切换,需要自定义组件实现LocaleResolver,并将其添加到spring容器中

  • 使用#{}进行取值

拦截器

  • 实现拦截器接口HandlerInterceptor,实现preHandle和postHandle接口的方法

    image-20210901160255460

  • 在自定义配置类继承WebMvcConfigurer中实现方法,注册拦截器

    image-20210901160411301

防止拦截静态资源:

  • 添加排除拦截registry.addInterceptor(new LoginHandlerInterceptor()).exclude(...)

  • 配置文件中添加spring.mvc.static-path-pattern,并将所有静态文件放置在/static路径下

    注意:使用这种方法在访问静态资源时,必须在static目录下新建一层static目录,静态资源路径默认不显示第一层static

文件上传

文件上传与MultipartAutoConfiguration相关,文件相关属性绑定:

1
2
3
4
5
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
1
2
3
4
5
6
<form th:action="@{/upload}" enctype="multipart/form-data" method="post">
<input type="file" name="headerImg"><br>
<!--多文件上传-->
<input type="file" name="photos" multiple><br>
<input type="submit">
</form>

只需要以MultipartFile接收传输的参数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//单文件及多文件上传
@RequestMapping("/upload")
public String upload(@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
if (!headerImg.isEmpty()){
String filename = headerImg.getOriginalFilename();
//这里使用静态路径
headerImg.transferTo(new File("D:\\cache\\"+filename));
}
if (photos.length>0){
for (MultipartFile photo: photos){
String filename = photo.getOriginalFilename();
photo.transferTo(new File("D:\\cache\\"+filename));
}
}
return "index";
}

错误处理

image-20210917152141146

具体实现在原理部分

注入原生组件

一般用于转化老项目

  • Servlet3.0注解:主程序类上注解

    1
    @ServletComponentScan(basePackage = "com.lan5th")

    组件类上注解@WebServlet, @WebFilter, @WebListener进行自动扫描注入容器

  • 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
    25
    26
    27
    28
    //由于SpringBoot内置了servlet容器,没有web.xml文件,因此使用替代方法:ServletRegistrationBean将需要的Servlet注册进容器
    //使用代理模式,生成单实例
    @Configuration(proxyBeanMethods = true)
    public class MyRegistConfig{
    @Bean
    public ServletRegistrationBean myServlet(){
    MyServlet myServlet = new MyServlet();
    return new ServletRegistrationBean<>(myServlet,"/myrequest");
    }

    @Bean
    public FilterRegistrationBean myFilter(){
    MyFilter myFilter = new MyFilter();
    //设置拦截的Servlet
    //FilterRegistrationBean bean = new FilterRegistrationBean(myFilter, myServlet());

    //或设置拦截url
    FilterRegistrationBean bean = new FilterRegistrationBean(myFilter);
    bean.setUrlPatterns(Arrays.asList("/myrequest","/css/"));
    return bean;
    }

    @Bean
    public ServletListenerRegistrationBean myListener(){
    MyListener myListener = new MyListener();
    return ServletListenerRegistrationBean(myListener)
    }
    }

    ServletRegistrationBean, FilterRegistrationBean, ListenerRegistrationBean

拓展

容器中的DispatcherServlet也是通过RegistrationBean的方法注册进来,对应配置文件的前缀spring.mvc,默认映射/路径

多个Servlet能够处理同一路径时,采用精确优先原则

因此当我们向容器中注册Servlet路径为/的子目录时默认以我们的Servlet更为优先

使用AOP

导包

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

切面类

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
//同时标注这两个注解才能生效
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(public * com.example.mybatisplus.web.controller.TestController.*(..))")
public void webLog(){}

@Before("webLog()")
public void deBefore(JoinPoint joinPoint) {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
System.out.println("URL : " + request.getRequestURL().toString());
System.out.println("HTTP_METHOD : " + request.getMethod());
System.out.println("IP : " + request.getRemoteAddr());
System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs()));

}

@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) {
// 处理完请求,返回内容
System.out.println("方法的返回值 : " + ret);
}

//后置异常通知
@AfterThrowing("webLog()")
public void throwss(JoinPoint jp){
System.out.println("方法异常时执行.....");
}

//后置最终通知,final增强,不管是抛出异常或者正常退出都会执行
@After("webLog()")
public void after(JoinPoint jp){
System.out.println("方法最后执行.....");
}

//环绕通知,环绕增强,相当于MethodInterceptor
@Around("webLog()")
public Object arround(ProceedingJoinPoint pjp) {
System.out.println("方法环绕start.....");
try {
Object o = pjp.proceed();
System.out.println("方法环绕proceed,结果是 :" + o);
return o;
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
}

jar包启动springboot

1
2
java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=配置的debug端口号 -Dspring.profiles.active=prod 包名.jar
java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8792 -Dspring.profiles.active=prod blog-0.0.1-SNAPSHOT.jar

数据访问

JDBC

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

切换mysql-connector-java版本方法:

  • 直接指定<version></version>

  • 由maven就近优先原则在最外层Pom中配置

    1
    2
    3
    4
    <properties>
    <java.version>1.8</java.version>
    <mysql.version>5.1.49</mysql.version>
    </properties>

一些相关的自动配置类:

image-20210917214258823

image-20210917214220691

配置项

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost:3306/jdbcstudy?useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver

Druid数据源

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

可以通过导入starter或自定义注入的方式导入数据源

配置完成druid后台监控之后可以根据配置的路径访问数据源后台监控页面

如:配置了url为/druid/*,项目启动之后我们在浏览器中访问http://localhost:8080/druid/就可以进行后台监控了

image-20210903162644116

自行导入

不进行配置这些设置会有默认值

由于Druid后台监控必须通过特定Servlet进行开启,因此选用ServletRegistrationBean方式进行注入

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
@Configuration
public class DruidConfig {
//自动装配DruidDataSource
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setFilters("stat,wall");
return druidDataSource;
};

//后台监控
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(),"/druid/*");

//后台需要登录,进行账号密码配置
HashMap<String, String> initParameters = new HashMap<>();
//登录的用户名和密码key是固定的,不能随意更改
//value与数据库无关,可以自定义配置
initParameters.put("loginUsername","admin");
initParameters.put("loginPassword","123456");

bean.setInitParameters(initParameters);
return bean;
}

//日志记录
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new WebStatFilter());
HashMap<String, String> initParameters = new HashMap<>();
//这些路径不进行过滤
initParameters.put("exclusions","*.js,*.css,/druid/*");
bean.setInitParameters(initParameters);
return bean;
}
}

当然,我们配置这些JavaBean的过程也可以用配置文件所代替

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
spring: 
datasource:
#SpringBoot默认是不注入这些的,需要自己绑定
#druid数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true

#配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
#则导入log4j 依赖就行
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
datasource:
druid:
username: root
password: Zyh20010605
url: jdbc:mysql://localhost:3306/lz_blog?useSSl=false&serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver

#连接池属性
initial-size: 15
max-active: 100
min-idle: 15
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
test-on-borrow: false
test-on-return: false
test-while-idle: true
validation-query: SELECT 1
validation-query-timeout: 1000
keep-alive: true
remove-abandoned: true
remove-abandoned-timeout: 180
log-abandoned: true
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
filters: stat,wall,slf4j
use-global-data-source-stat: true
maxOpenPreparedStatements: 100
connect-properties.mergeSql: true
connect-properties.slowSqlMillis: 5000

# 配置DruidStatFilter
web-stat-filter:
enabled: true
url-pattern: "/*"
exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
# 配置DruidStatViewServlet
stat-view-servlet:
url-pattern: "/druid/*"
# IP白名单(没有配置或者为空,则允许所有访问)
allow: 127.0.0.1
# IP黑名单 (存在共同时,deny优先于allow)
deny: 192.168.0.1
# 禁用HTML页面上的“Reset All”功能
reset-enable: false
# 登录名
login-username: admin
# 登录密码
login-password: 123456
# 新版需要配置这个属性才能访问监控页面
enabled: true

Starter方式导入

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

start方式也需要配置类来注入

1
2
3
4
5
6
7
8
9
10
@Configuration
public class DruidConfig {
//自动装配DruidDataSource
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
datasource:
druid:
filters: stat,wall,log4j #监控sql,防火墙,日志
stat-view-servlet: #监控页
enabled: true
login-username: admin
login-password: 123456
web-stat-filter: #监控web
enabled: true
url-pattern: /*
exclusions: '*.js,*.css,/druid/*'
aop-patterns: com.lan5th #监控Bean
filter: #对于上面filters的详细配置
stat:
slow-sql-millis: 1000
log-slow-sql: true
wall:
enabled: true

Mybatis

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

注册Mapper方式

  • 在Mapper类上方添加@Mapper注解
  • 在主类上方添加@MapperScan("com.lan5th.mapper")自动扫描包注解

配置文件

1
2
3
4
5
6
mybatis:
type-aliases-package: com.lan5th.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
config-location: classpath:mybatis/mybatis-config.xml #配置文件可以不配置,而是使用下面的方式进行配置
configuration: #代替配置文件的功能
map-underscore-to-camel-case: true

一般方式

使用方式与之前相似

1
2
3
4
5
//如果是包扫描方式需要@Repository注解
@Mapper
public interface UserMapper {
public User selectUser();
}

Mapper.xml需要在配置文件中所确定的路径下进行编写,不能像之前放在Mapper类的同级目录下

然后在对应的xxxMapper.xml中编写sql

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lan5th.dao.StudentMapper">
<select id="getStudentList" resultType="Student">
select * from mybatis.student;
</select>
</mapper>

注解方式

直接在mapper接口的方法中添加sql相关注解

1
2
3
4
5
@Mapper
public interface UserMapper {
@Select("select * from User;")
public User selectUser();
}

当然也可以使用混合方法,即两种方法同时使用

使用方法:

  • 引入mybatis-spring-boot-starter
  • 配置application.yaml指明mapper-location位置
  • 编写Mapper接口,标注@Mapper注解(或使用包扫描)
  • 简单操作使用注解方式(如单表select)
  • 复杂操作使用mapper.xml进行绑定映射

MybatisPlus

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

MybatisPlusProperties已经默认配置好了mapper映射文件的路径

1
private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"};

即任意路径下的mapper包都会被扫描

image-20210918114811888

Mapper接口继承BaseMapper,其中已经实现了一些简单的CRUD方法

UserMapper.java继承BaseMapper<>

1
2
@Mapper
public interface UserMapper extends BaseMapper<User> {}

UserService.java继承IService<>

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

UserServiceImpl.java继承ServiceImpl<>

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

分页功能

1
2
3
4
5
6
7
8
@RequestMapping("/getUser")
public String getUserList(@RequestParam(value = "pn", defaultValue = "1") Integer pageNo,
Model model){
Page<User> userPage = new Page<>(pageNo, 10);
Page<User> page = userService.page(userPage);
model.addAttribute("page", page);
return "/tablePage";
}

Redis

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 配置文件

    1
    2
    3
    4
    5
    spring:
    redis:
    host: 47.113.225.244
    port: 6379
    password: xxxxxx #这个是redis-server的密码
  • redis需要的连接工厂已经自动配置完成:LettuceConnectionConfiguraion和JedisConnectionConfiguration

  • Redis模板

    • RedisTemplate:可以实现Redis基本操作
    • StringRedisTemplate:由于String类型是常用类型,因此springboot将其提取为一个单独的组件
  • SpringBoot2.0以上默认使用lettuce作为默认客户端,如果要使用jedis,需要自行导入jedis的依赖包并在配置文件中手动指定客户端类型spring.redis.client-type

操作实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Autowired
private RedisTemplate redisTemplate;

@Test
void redisTest(){
//获取连接对象
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushDb();

//先用opsForXXX()获取专门处理某类业务的工具
//如valueOperations专门操作字符串
ValueOperations valueOperations = redisTemplate.opsForValue();
//再调用valueOperations的方法来执行具体操作
valueOperations.set("key","value");
System.out.println(valueOperations.get("key"));

}

阿里云redis连接失败的原因

  • 阿里云安全组策略是否开启对应端口?
  • redis-server配置文件中是否绑定0.0.0.0?
  • server密码问题
  • 服务器防火墙是否开放对应端口,如CentOS7系统:
    • 开放防火墙对应端口firewall-cmd --zone=public --add-port=6379/tcp --permanent
    • 查看端口开放情况netstat -ntlp

连接失败与protected-mode联系不大,请勿轻易关闭

单元测试Junit5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--兼容Junit4-->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
  • 编写测试方法:@Test注解(使用Junit5的注解)
  • Junit具有Spring的功能如@AutoWired@Transactional测试方法,完成后自动回滚

常用测试注解

1
2
3
4
5
6
7
8
9
10
11
12
@Test //单元测试注解
@ParameterizedTest //参数化测试
@RepeatedTest //方法可重复执行
@DisplayName //为测试类或方法设置展示名称
@BeforeEach //每个单元测试之前执行
@AfterEach //每个单元测试之后执行
@BeforeAll //所有单元测试之前执行(必须static)
@AfterAll //所有单元测试之后执行(必须static)
@Tag //表示单元测试类型
@Disabled //表示测试方法不执行
@Timeout //表示测试方法超过规定时间就会返回异常
@ExtendWith //为测试类或方法提供扩展类引用,如使用SpringBoot自动注入功能 @ExtendWith(SpringExtension.class)

断言机制

检查业务逻辑返回的数据是否合理,所有测试运行结束后会有一个详细的运行报告

简单断言

使用Assertions包下的相关方法,也可以直接导入Assertions的静态方法,在编写测试类时直接调用

image-20210918170825707

1
2
3
4
5
6
7
8
9
@Test
void test1(){
int res = cal(1, 2);
Assertions.assertEquals(3,res,"业务逻辑断言失败");
}

int cal(int i, int j){
return i+j;
}

一个断言失败,这个方法中后续的所有代码都不会执行

组合断言

1
2
3
4
5
6
7
//这里的静态方法都已经import调用
@Test
void test2(){
Assertions.assertAll("test",
()-> assertTrue(true&&true),
()-> assertEquals(1, 2));
}

异常断言

1
2
3
4
5
6
7
@Test
void test3(){
//抛出异常表示断言成功,无异常表示断言失败,输出响应提示信息
assertThrows(ArithmeticException.class,
()->{int i = 10/0;},
"业务逻辑居然正常运行");
}

快速失败

1
2
3
4
5
6
7
@Test
void test4(){
//实际上if条件为真不论其他代码怎样都会直接失败
if (1 == 2){
fail("测试失败");
}
}

前置条件

1
2
3
4
5
6
@Test
void test5(){
//前置条件失效,会skip错误,而不是和断言错误汇总在一起
Assumptions.assumeTrue(false, "结果不是true");
System.out.println("111111");
}

嵌套测试

嵌套测试中,外层Test不能驱动内层Test的@BeforeEach之类的方法,内层的Test能够驱动外层Test的@BeforeEach方法

嵌套测试即多个测试内部类层层嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class OuterTest{
@Test
void outertest(){
int res = cal(1, 2);
assertEquals(3,res);
}

@Nested
class MiddleTest{
@Test
void middleTest(){
assertEquals(2,2);
}

@Nested
class innerTest{
@Test
void innerTest(){
assertEquals(3,3);
}
}
}
}

参数化测试

静态数据参数

1
2
3
4
5
@ParameterizedTest
@ValueSource(ints = {1,2,3,4,5})
void test6(int i){
System.out.println(i);
}

方法返回值参数

1
2
3
4
5
6
7
8
9
10
@ParameterizedTest
@MethodSource("method")
void test6(String i){
System.out.println(i);
}

//方法必须为静态,且返回流数据
static Stream<String> method(){
return Stream.of("apple","banana","lan5th");
}

指标监控

开启Actuator

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

actuator的所有监控功能称为EndPoints,开启所有功能即暴露所有端点信息

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

最常用的EndPoint

  • health
  • Metric
  • loggers

SpringBootAdmin

是一个开源的后台监管应用程序,如果添加了SpringSecurity,还需要作一些其他配置

新建一个springboot项目作为adminServer服务端,只需要引入这两个依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

主程序类添加注解

1
2
3
4
5
6
7
8
9
@EnableAdminServer
@SpringBootApplication
public class AdminserverApplication {

public static void main(String[] args) {
SpringApplication.run(AdminserverApplication.class, args);
}

}

防止端口冲突更改端口号server.port=8888

为我们需要监控的项目添加依赖作为adminServer客户端

1
2
3
4
5
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.5.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
spring:
boot:
admin:
client:
url: http://localhost:8888 #指定adminServer的ip和端口
instance:
prefer-ip: true #使用ip金星识别,否则会显示计算机名
application:
name: mainServer #应用程序名称

深层原理剖析

自定义Starter

新建项目包含一个maven模块和SpringBoot模块

mystarter-spring-boot-starter

starter负责导入Autoconfiguration模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>mystarter-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>com.lan5th</groupId>
<artifactId>mystarter-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

mystarter-spring-boot-starter-autoconfigure

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lan5th</groupId>
<artifactId>mystarter-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mystarter-spring-boot-starter-autoconfigure</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>

首先我们定义一个逻辑应用HelloService

1
2
3
4
5
6
7
8
public class HelloService {
@Autowired
private HelloProperties helloProperties;

public String sayHello(String userName){
return helloProperties.getPrefix()+userName+helloProperties.getSuffix();
}
}

HelloService需要HelloServiceAutoConfiguration来给容器中进行注入,同时HelloServiceAutoConfiguration会绑定HelloProperties来读取配置文件的信息

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
@ConditionalOnMissingBean(HelloService.class)
@Bean
public HelloService helloService(){
HelloService helloService = new HelloService();
return helloService;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ConfigurationProperties("lan5th.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;
}
}

image-20210918230038309

我们还需要在这个路径下新建spring.factories来告诉springboot应该导入哪些自动配置类

1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lan5th.mystarterspringbootstarterautoconfigure.auto.HelloServiceAutoConfiguration

在starter和autoconfigure编写完成后使用maven操作clean和install,此时我们自己编写的包就被安装在了本地的maven库,可以供其他程序进行调用

实际应用程序

引入依赖

1
2
3
4
5
<dependency>
<groupId>com.lan5th</groupId>
<artifactId>mystarter-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

通过配置文件来注入属性

1
2
3
4
lan5th:
hello:
prefix: '你好'
suffix: '再见'

测试Controller进行调用

1
2
3
4
5
6
@ResponseBody
@RequestMapping("/hello")
public String sayHello(){
String hello = helloService.sayHello("张三");
return hello;
}

实际效果

image-20210918230441414

到此我们已经能够编写我们自己的starter来适应各种情况了!

Springboot启动原理

getSpringFactoriesInstances()一般都指从各个依赖的spring.factories文件中取值

创建SpringApplication

  • 保存一些信息

  • 使用ClassUtils判断当前应用类型

  • 寻找Bootstrappers:初始化引导器

    获取List<Bootstrapper>:从spring.factories中寻找

  • 寻找ApplicationContextInitializer:初始化器

    获取List<ApplicationContextInitializer<?>>spring.factories中寻找

  • 寻找ApplicationListener:应用监听器

    获取List<ApplicationListener>spring.factories中寻找

运行SpringApplication

  • StopWatch

  • 记录应用启动时间

  • 创建引导上下文createBootstrapContext()

    • 获取之前所有的Bootstrappers遍历执行inititialize()来完成对引导启动器的上下文环境配置
    • 让当前应用进入headless模式java.awt.headless
  • 获取所有的SpringApplicationRunListener:运行监听器

    • 获取List<SpringApplicationRunListener>spring.factories中寻找

    • 遍历所有的SpringApplicationRunListener调用starting方法

      相当于通知所有关注系统启动过程的Listener进行监听

  • 保存命令行参数:ApplicationArgument

  • 准备环境:prepareEnvironment()

    • 返回或创建基础环境信息:StandardServletEnvironment
    • 配置环境信息:读取所有的配置源属性的配置属性值(包括命令行和外部配置文件)
    • 绑定环境信息
    • 监听器调用environmentPrepared():通知所有Listener当前环境准备完成
  • 创建IOC容器createApplicationContext()

    • 根据当前项目类型创建容器(当前为servlet),因此会创建AnnotationConfigServletWebServerApplicationContext
  • 准备ApplicationContext IOC容器的基本信息 prepareContext()

    • 保存环境信息

    • IOC容器的后置处理流程

    • 应用初始化器applyInitializers()

      • 遍历所有的ApplicationContextInitializer,调用initialize方法对IOC容器进行初始化扩展功能

      • 调用所有的监听器的contextPrepared(),EventPublishRunListener(),通知所有的Listener上下文准备完成

    • 调用所有的监听器的contextLoaded()通知所有Listener上下文已经加载完成

  • 刷新IOC容器refreshContext()

    • 创建容器中的所有组件Bean(Spring注解)
  • 调用所有的监听器的started(context)方法,通知所有Listener项目已经启动

  • 调用所有的Runners:callRunners()

    • 获取容器中的ApplicationRunner
    • 获取容器中的CommandLineRunner
    • 合并所有Runner并按照@Order进行排序
    • 遍历所有的Runner,调用run()方法
  • 调用所有的监听器的running()方法,通知Listener应用开始运行

  • 如果以上出现任何异常,调用所有的监听器的failed()方法,通知Listener出现异常

自定义Listener

1
2
3
4
5
6
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
System.out.println("MyApplicationContextInitializer is running......");
}
}
1
2
3
4
5
6
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
System.out.println("MyApplicationListener is running......");
}
}
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
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
private SpringApplication application;
public MySpringApplicationRunListener(SpringApplication application, String[] args){
this.application = application;
}

@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MySpringApplicationRunListener is starting......");
}

@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MySpringApplicationRunListener is environmentPrepared......");
}

@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener is contextPrepared......");
}

@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener is contextLoaded......");
}

@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener is started......");
}

@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener is running......");
}

@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MySpringApplicationRunListener is failed......");
}
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 运行开始时的一次性业务
* MyApplicationRunner是从容器中获取因此不需要给spring.factories中添加配置
*/
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner is running......");
}
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 运行开始时的一次性业务
* MyCommandLineRunner是从容器中获取因此不需要给spring.factories中添加配置
*/
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner is running......");
}
}

spring.factories

1
2
3
4
5
6
7
8
9
10
11
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
com.lan5th.listener.MySpringApplicationRunListener

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
com.lan5th.listener.MyApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
com.lan5th.listener.MyApplicationListener