SpringBoot框架入门与理解
SpringBoot框架入门与理解

SpringBoot框架入门与理解

前言

  • 学完SSM框架及其整合之后,我的感触是 “还挺方便”。能省去冗长的请求映射器、监听器、拦截器手动xml注册;能省去很难又不好记的JDBCUtils工具类;能省去手撸原生servlet代码、脚踏老坛酸菜 JavaWeb三层架构等等…
  • 直到我学完了SpringBoot…
  • 什么?不用写配置文件(SSM那配置文件…)?
  • 什么?依赖不用写version(version导致的bug懂得都懂,当然第三方还是要滴)?
  • 什么?不会真有人手动配Tomcat吧?…
  • SpringBoot框架不愧是java搭建服务器的一大神作,几乎不需手动配置的便捷特性能让我们把精力都放在业务逻辑的编写上;庞大的spring-boot-starter-parent库涵盖了大部分已整合好的dependency;丰富的注解驱动在处理业务逻辑上都能极大地便利我们。甚至在整合mybatis-plus后,实现简单的客户端服务器交互CRUD,dao和service层可以不放任何东西…
  • 所以,游戏里的你 使用SpringBoot的你,真的很强大!
    注:本文为原创内容,文字内容大部分为学习笔记、代码内容均为亲自手撕。文章经过CRUD处理,通俗易懂,很适合springboot入门的xdm。(标记*为重要代码应用处)

    CSDN本人原文链接:SpringBoot框架入门与理解

一、SpringBoot简介

1、SpringBoot能做什么

  • web开发 数据访问 安全控制 分布式 消息服务 移动开发 批处理...

2、为什么使用SpringBoot

  • 为了省去类似SSM整合复杂的配置文件过程,SpringBoot拥有自动配置文件的功能
  • 能快速创建出生产级别的Spring应用

Springframework的底层是java代码

SpringBoot的底层是Springframework

(框架的框架)

2.1 SpringBoot的优点

  • 创建独立Spring应用(不影响原有Spring项目同时,增加新功能)

  • 内嵌web服务器(不需手动配置Tomcat服务器)

  • 自动starter依赖,简化构建配置 (自动控制jar包依赖版本)

  • 自动配置Spring以及第三方功能(自动配置如ssm的xml文件)

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

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

总结:

SpringBoot是整合Spring技术钱的一站式框架

SpringBoot是简化Spring技术栈的快速开发脚手架

2.2 SpringBoot的缺点

版本迭代快,内部原理相对复杂

3、时代背景

3.1 微服务

微服务是一种架构风格

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

3.2 分布式

分布式的困难: 远程调用、服务发现、负载均衡、 服务容错、配置管理、服务监控、链路追踪、曰志管理 、任务调度...

分布式的解决:SpringBoot + SpringCloud

二、SpringBoot入门

1、HelloWorld*

1.1 引入maven依赖

<!--Spring Boot的父级依赖,表明当前项目为Spring Boot项目
依赖自动管理,引入后其他依赖不需要添加<version>标签
-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

</dependencies>

<!--将项目打包为jar包-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

1.2 直接开撸代码,创建主程序

/*表明为SpringBoot应用*/
@SpringBootApplication
public class MainApplication {

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

1.3 创建业务

/*
@ResponseBody 所有方法响应浏览器
@Controller 标明所有方法为控制器方法
*/
@RestController //@ResponseBody+@Controller
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        return "hello SpringBoot";
    }
}

1.4 测试

直接运行main方法

在网页中输入localhost:8080(端口号)/页面

1.5 SpringBoot配置文件

在resources资源包下创建application.properties文件

可在其中更改端口号、文件编码、日志文件、缓存...

三、了解自动配置原理

1、SpringBoot特性

1.1 依赖管理

  • 父项目做依赖管理
依赖管理    
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
</parent>

他的父项目
 <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.3.4.RELEASE</version>
  </parent>

几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制

SpringBoot依赖的树状

  • 导入starter场景启动器
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 可修改默认版本号
<properties>
    <!--可自定义依赖版本号,修改mysql依赖为8.0.28版本-->
    <mysql.version>8.0.28</mysql.version>
</properties>

1.2 自动配置

  • 自动配置好了Tomcat
    • 在spring-boot-starter-web包中:
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <version>2.3.4.RELEASE</version>
      <scope>compile</scope>
</dependency>
  • 自动配好SpringMVC

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

    • SpringBoot帮我们配置好了所有web开发的常见场景
  • 默认的包结构

    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
    • Controller控制器最好放在主程序的同包或同包的子包中
    • 无需以前的包扫描配置
    • 想要改变扫描路径,@SpringBootApplication(scanBasePackages="com")或者@ComponentScan("com") 指定扫描路径
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com") //自定义扫描路径
    public class MainApplication {main}
  • 各种配置拥有默认值

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

    • 非常多的starter
    • 引入了哪些场景这个场景的自动配置才会开启
    • SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面
  • ......

2、容器功能

2.1 组件添加*

@Configuration

@Configuration(proxyBeanMethods = true(default)/false)

  • Configuration(proxyBeanMethods = true)【保证每个@Bean方法被调用多少次返回的组件都是单实例的】

  • Configuration(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】

    组件依赖必须使用Full模式默认。其他默认是否Lite模式

/*表明为配置类*/
/**
 * 配置类中使用@Bean注解在方法上给容器注册组件,默认是单实例
 * 配置类本身也是组件,可被获取
 */
@Configuration
public class MyConfig {

    /*配置组件*/
    @Bean
    public User user1(){
        return new User("蔡徐坤",20);
    }

    @Bean("Dog") //自定义组件名称
    public Pet pet1(){
        return new Pet("二哈");
    }
}
  • 在MainApplication主程序中获取组件
public static void main(String[] args) {
    /*返回IOC容器*/
    ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

    /*查看容器里的所有组件*/
    /*String[] names = run.getBeanDefinitionNames();
    for(String name:names){
        System.out.println(name);
    }*/

    /*从容器中获取组件*/
    Pet dog = run.getBean("Dog",Pet.class); //更改组件名称后,获取时要指定组件名称
    System.out.println(dog);

    User user1 = run.getBean("user1", User.class); 
    System.out.println(user1);
}

@Bean

配置类中使用@Bean注解在方法上给容器注册组件

获取组件时默认为方法名,可用@Bean("") 来自定义名称

SSM中的注解

  • 用来创建Bean实例:

@Component,@Controller,@Service,@Repository

  • 设置扫描组件:

@ComponentScan

@Import

  • @Import({User.class,Pet.class}) 给容器中自动注册这两个类的组件
@Import({User.class,Pet.class})
@Configuration
public class MyConfig {

    /*配置组件*/
    @Bean
    public User user1(){
        return new User("蔡徐坤",20);
    }
}
  • 在主程序中测试
System.out.println("---------------------User---------------------");
String[] beanNamesForType = run.getBeanNamesForType(User.class);
for(String names:beanNamesForType){
    System.out.println(names);
}
  • 结果:

第一个为@Import自动注册的组件,默认名为全类名

第二个为@Bean手动注册的组件,默认名为方法名或自己更改的名称

@Import 高级用法: https://www.bilibili.com/video/BV1gW411W7wy?p=8

@Conditional

条件装配:满足Conditional指定的条件,则进行组件注入

  • @ConditionalOnBean(name="Dog") : 当容器中有名为“Dog”的组件时,其他组件注册才会生效
  • @ConditionalOnMissingBean(name="Dog") 当容器中没有名为“Dog”的组件时,其他组件注册才会生效
@ConditionalOnBean(name="Dog")/@ConditionalOnMissingBean(name="Dog")
public class MyConfig {
    @Bean("Dog") //自定义组件名称
    public Pet pet1(){
        return new Pet("二哈");
    }
}

2.2 原生配置文件引入

@ImportResource

例如在resources中有原生的spring配置文件

<bean id="test" class="com.boot.bean.User">
        <property name="name" value="蔡徐坤"></property>
        <property name="age" value="18"></property>
</bean>

但SpringBoot程序并不认识它,所以将其导入到容器中

@ImportResource("classpath:beans.xml")
public class MyConfig {}

2.3配置绑定*

读取properties文件中的内容,并且把它封装到JavaBean中,以供随时使用;

  • application.properties
mycar.brand=Mercedes Benz
mycar.price=800000

方式一:@Component + @ConfigurationProperties

  • Bean
@Component
@ConfigurationProperties(prefix ="mycar") 
public class MyCar {
    private String brand;
    private Integer price;
    //get,set...
}
  • 测试
@RestController //@ResponseBody+@Controller
public class HelloController {
    @Autowired
    MyCar myCar;

    @RequestMapping("/myCar")
    public MyCar myCar() {
        return myCar;
    }
}
  • 网页中显示: {"brand":"Mercedes Benz","price":800000}

方式二:@EnableConfigurationProperties + @ConfigurationProperties

  • Bean
@ConfigurationProperties(prefix ="mycar") 
public class MyCar {
    private String brand;
    private Integer price;
    //get,set...
}
  • MyConfig
@EnableConfigurationProperties(MyCar.class) //表明需要注册组件的类型
@Import({User.class,Pet.class})
@Configuration
public class MyConfig {}
  • 网页中显示: {"brand":"Mercedes Benz","price":800000}

3、自动配置原理

3.1 引导加载自动配置类*

  • @SpringBootApplication:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {}

分别解释三个注解:

@SpringBootConfiguration

代表当前是一个配置类

@ComponentScan

指定扫描组件

@EnableAutoConfiguration

@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {}
  • @AutoConfigurationPackage

自动配置导包,指定默认的导包规则

@Import({Registrar.class})
public @interface AutoConfigurationPackage {
//利用Registrar给容器中导入一系列组件
//将指定的一个包下的所有组件导入进MainApplication所在包下。
}
  • @Import({AutoConfigurationImportSelector.class})

1、利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件
2、调用List configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map<String, List> loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件
4、从META-INF/spring.factories位置来加载一个文件。默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件。spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

3.2 按需开启自动配置项

虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则(@Conditional),最终会按需配置。

3.3 修改默认配置*

@ConditionalOnBean(name="Dog") 当容器中有名为“Dog”的组件时,其他组件注册才会生效

@ConditionalOnMissingBean(name="Dog") 当容器中没有名为“Dog”的组件时,其他组件注册才会生效

SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先

总结:

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

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

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

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

  • 定制化配置

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

4、开发小技巧*

4.1 Lombok

简化java开发,帮助javaBean类自动写好构造方法、get、set、toString方法

  • 在pom.xml中导入lombok依赖
<!--引入lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
  • 在IDEA中引入插件

  • 在JavaBean类上写入注解
@AllArgsConstructor //生成所有参数的有参构造器
@NoArgsConstructor //生成无参构造器
@ToString //自动生成toString方法
@Data //自动生成get,set方法
public class User {
    private String name;
    private int age;
}

若是想创建部分属性的构造函数,需自己手动写入

简化日志开发:@Slf4j

@Slf4j
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello() {
        log.info("hello");
        return "hello SpringBoot";
    }
}

4.2 devtools

更改服务器代码后,可以使用快捷键Ctrl+F9来重启页面

<!--引入devtools-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>

4.3 Spring Initializr(项目初始化向导)

创建SpringBoot项目时可以一键设置项目所需依赖,工具,版本...

  • 创建Spring Initializr项目

  • 选择开发场景

  • 自动导入项目依赖及工具

四、配置文件

1、文件类型

1.1 properties

过去所用的配置文件类型

1.2 yml*

YAML 是 "YAML Ain't Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)

非常适合用来做以数据为中心的配置文件

基本语法

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

数据类型

  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
  • 对象:键值对的集合。map、hash、set、object
行内写法:  k: {k1:v1,k2:v2,k3:v3}
#或
k: 
  k1: v1
  k2: v2
  k3: v3
  • 数组:一组按次序排列的值。array、list、queue
行内写法:  k: [v1,v2,v3]
#或者
k:
 - v1
 - v2
 - v3

示例

将application.yml配置文件内容引入Person类的属性

  • Person类
/*引入yml配置文件内容*/
@Component
@ConfigurationProperties(prefix="person")
@Data //get,set
@ToString
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;
}
  • application.yml(也可以application.yaml)
person:
  userName: 蔡徐坤
  boss: true
  birth: 2001/4/1
  age: 20
  pet:
    name: 哈哥
    weight: 20
#  interests: [唱,跳,rap,篮球]
  interests:
    - 唱
    - 跳
    - rap
    - 篮球
#  animal: [哈士奇,萨摩耶,柴犬]
  animal:
    - 哈士奇
    - 萨摩耶
    - 柴犬
  score: {Chinese: 80,Math: 96,English: 88}
  salarys: [8000,8200,9000]
  allPets:
    white:
      - {name: s1,weight: 20}
      - {name: s2,weight: 30}
    black:
      - name: s3
        weight: 20
      - name: s4
        weight: 50

字符串也可以加单引号或双引号

  • 单引号会将 \n 作为字符串输出
  • 双引号会将 \n 作为换行输出

2、配置提示*

在properties或yaml文件中绑定自定义类一般没有提示,这时可引入配置提示依赖,在书写时有提示功能

<!--绑定配置提示-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

为了防止打包时将配置提示也打包,可引入插件

<!--打包时不用将配置处理器打包-->
<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>

五、Web开发

1、SpringMVC自动配置

大多场景我们都无需自定义配置

  • 内容协商视图解析器和BeanName视图解析器
  • 静态资源(包括webjars)
  • 自动注册 Converter,GenericConverter,Formatter
  • 支持 HttpMessageConverters
  • 自动注册 MessageCodesResolver (国际化用)
  • 静态index.html 页支持
  • 自定义 Favicon
  • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

2、简单功能分析

2.1 静态资源访问

静态资源目录*

只要静态资源放在类路径下: /static /public /resources /META-INF/resources

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

localhost:8080/(资源名)

原理: 静态映射/**。

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

静态资源访问前缀*

#设置静态资源访问前缀
spring:
  mvc:
    static-path-pattern: /static/**

localhost:8080/static.(资源名)

webjar

引入jquery依赖

 <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.5.1</version>
</dependency>

访问(http://localhost:8080/webjars/jquery/3.5.1/jquery.js)可查看jquery资源

2.2 设置index页面

可以在任何静态资源目录下创建index.html页面

访问localhost:8080即可显示index.html页面

2.3 自定义favicon

将favicon.ico图片放在静态资源目录,该项目的页面图标就是这张图片

2.4 静态资源配置原理

  • SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
  • SpringMVC功能的自动配置类 WebMvcAutoConfiguration,生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
  • 禁用掉所有静态资源,使静态资源不能被访问
spring:
  web:
    resources:
      add-mappings: false

3、请求参数处理

3.1 Rest使用与原理*

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

    • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser 保存用户
    • 现在: /user GET-获取用户 /user DELETE-删除用户 /user PUT-修改用户 /user POST-保存用户
    • 核心Filter;HiddenHttpMethodFilter
      • 用法: 表单method=post,隐藏域 _method=put
    • SpringBoot中手动开启

使用Rest方法进行表单提交,并请求到指定的映射上

  • 需要在application.yaml中开启Rest风格
#开启REST风格
spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true
  • Controller
@RestController
public class MyController {

    /*@RequestMapping(value = "/user",method = RequestMethod.GET)*/
    @GetMapping("/user")
    public String getUser(){
        return "GET-张三";
    }

    /*@RequestMapping(value = "/user",method = RequestMethod.POST)*/
    @PostMapping("/user")
    public String saveUser(){
        return "POST-张三";
    }

    /*@RequestMapping(value = "/user",method = RequestMethod.PUT)*/
    @PutMapping("/user")
    public String putUser(){
        return "PUT-张三";
    }

    /*@RequestMapping(value = "/user",method = RequestMethod.DELETE)*/
    @DeleteMapping("/user")
    public String deleteUser(){
        return "DELETE-张三";
    }
}
  • index.html
<form action="/user" method="get">
    <input type="submit" value="GET">
</form>
<br>
<form action="/user" method="post">
    <input type="submit" value="POST">
</form>
<br>
<form action="/user" method="post">
    <input type="hidden" name="_method" value="PUT">
    <input type="submit" value="PUT">
</form>
<br>
<form action="/user" method="post">
    <input type="hidden" name="_method" value="DELETE">
    <input type="submit" value="DELETE">
</form>

Rest原理(表单提交要使用REST的时候)

  • 表单提交会带上_method=PUT

  • 请求过来被HiddenHttpMethodFilter拦截

    • 请求是否正常,并且是POST
      • 获取到_method的值。
    • 兼容以下请求;PUT.DELETE.PATCH
    • 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
    • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用**requesWrapper的。**

Rest使用客户端工具

  • 如PostMan直接发送Put、delete等方式请求,无需Filter

使用Rest风格更改form表单中type=hidden的name属性(原来为"_method")

  • 在配置类中
@Configuration
public class MyConfig {

    /*自定义Rest-form-hidden-name*/
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
        HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter();
        filter.setMethodParam("_m");
        return filter;
    }
}
  • form表单中可以填写name="_m"
<form action="/user" method="post">
    <input type="hidden" name="_m" value="PUT">
    <input type="submit" value="PUT">
</form>

3.2 请求映射原理

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

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

所有的请求映射都在HandlerMapping中。

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;

  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。

    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMappinh

3.3 普通参数与基本注解*

@PathVariable

获取请求中占位符的值

@RequestHeader

获取请求头信息

@RequestParam

获取请求参数

@CookieValue

获取Cookie信息

@RequestBody

获取请求体

@RestController
public class ParamTestController {

    @GetMapping("car/{id}/owner/{username}")
    public Map<String,Object> getCarInformation(
    @PathVariable("id") String id, //获取请求中占位符的值
    @PathVariable("username") String username,
    @PathVariable Map<String,String> pm, //通过map集合的方式获占位符的值
    @RequestHeader("Host") String requestHeader_Host, //获取指定key的请求头信息
    @RequestHeader Map<String,String> allHeader, //获取所有请求头信息
    @RequestParam("age") Integer age, //获取请求参数
    @RequestParam("interest") String interest,
    @RequestParam Map<String,String> allParams, //获取所有请求参数的值
    @RequestBody String requestBody //获取请求体

    ){
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("id",id);
        hashMap.put("username",username);
        hashMap.put("ParamMap",pm);
        hashMap.put("Host",requestHeader_Host);
        hashMap.put("HeaderMap",allHeader);
        hashMap.put("age",age);
        hashMap.put("interest",interest);
        hashMap.put("AllParamMap",allParams);
        hashMap.put("requestBody",requestBody);
        return hashMap;
    }
}

@RequestAttribute

获取request域属性

请求参数处理,可以将保存在request域中的参数通过key获取

@Controller
public class RequestController {

    @GetMapping("/forwardSuccess")
    public String forwardSuccess(HttpServletRequest request){
        request.setAttribute("username","蔡徐坤");
        request.setAttribute("password","123123123");
        return "forward:/success"; //转发到success请求
    }

    @ResponseBody
    @GetMapping("/success")
    public Map<String,Object> success(@RequestAttribute("username") String username,
                                      @RequestAttribute("password") String password){
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("username",username);
        hashMap.put("password",password);
        return hashMap;
    }
}

@MatrixVariable

处理矩阵变量

使用前,需要在配置类取消SpringBoot自动移除删除符后内容,矩阵变量功能才能生效

@Configuration
public class MyConfig implements WebMvcConfigurer {
    /*取消SpringBoot自动移除删除符后内容,矩阵变量功能才能生效*/
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}
  • 创建矩阵变量方式的超链接
<p><a class="btn btn-primary btn-lg" href="/cars/sell;low=20;brand=bc,bm,ad" role="button">@MatrixVariable(矩阵变量1)</a></p>

<br><br>

<p><a href="/person/1;age=20/2;age=30" class="btn btn-primary btn-lg" role="button">@MatrixVariable(矩阵变量2)</a></p>
  • 创建请求控制器
/* /cars/sell;low=30;brand=lbjn,bl,lsls */
@GetMapping("/cars/{path}")
public Map<String,Object> matrixVariableTest1(
        @MatrixVariable("low") String low,
        @MatrixVariable("brand") List<String>brand,
        @PathVariable("path") String path
){
    HashMap<String, Object> hashMap = new HashMap<>();
    hashMap.put("low",low);
    hashMap.put("brand",brand);
    hashMap.put("path",path);
    return hashMap;
}

/* /person/1;age=20/2;age=30 */
@GetMapping("/person/{boss}/{employee}")
public Map<String,Object> matrixVariableTest2(
        @MatrixVariable(value="age",pathVar = "boss") Integer bossAge,
        @MatrixVariable(value ="age",pathVar = "employee") Integer employeeAge
){
    HashMap<String, Object> hashMap = new HashMap<>();
    hashMap.put("bossAge",bossAge);
    hashMap.put("employeeAge",employeeAge);
    return hashMap;
}

3.4 ServletAPI

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

ServletRequestMethodArgumentResolver 能够解析以上的部分参数

@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) ||
                InputStream.class.isAssignableFrom(paramType) ||
                Reader.class.isAssignableFrom(paramType) ||
                HttpMethod.class == paramType ||
                Locale.class == paramType ||
                TimeZone.class == paramType ||
                ZoneId.class == paramType);
}

3.5 Model,Map,HttpServeltRequest*

都是给request域中放数据

@RequestMapping("/mapTest")
    public String mapTest(Model model,
                          Map<String,Object> map,
                          HttpServletRequest request){
        model.addAttribute("empName","CXK");
        map.put("bossName","LBW");
        request.setAttribute("queenName","QBL");
        return "forward:/mapTestRes";
    }

    @ResponseBody
    @RequestMapping("/mapTestRes")
    public Map<String,Object> mapTestRes(
            @RequestAttribute("empName") String empName,
            @RequestAttribute("bossName") String bossName,
            @RequestAttribute("queenName") String queenName){
        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("empName",empName);
        hashMap.put("bossName",bossName);
        hashMap.put("queenName",queenName);
        return hashMap;
    }

3.6 自定义对象参数*

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

  • Bean
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String username;
    private Integer age;
    private String birth;
    private Pet pet;
}
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Pet {
    private String name;
    private Integer age;
}
  • 在页面输入数据
<form action="/saveUser" method="post">
<input type="text" class="form-control" name="username" id="saveUser1" placeholder="姓名">
<input type="text" class="form-control" name="age" id="saveUser2" placeholder="年龄">
<input type="text" class="form-control" name="birth" id="saveUser3" placeholder="生日">
<input type="text" class="form-control" name="pet.name" id="saveUser4" placeholder="宠物名">
<input type="text" class="form-control" name="pet.age" id="saveUser5" placeholder="宠物年龄">
<button type="submit" class="btn btn-default">SUBMIT</button>
</form>
  • 获取数据并封装为User类
/*封装为User类*/
@RequestMapping("/saveUser")
public User saveUser(User user){
    return user;
}

3.7 自定义Converter

  • MyConfig中:

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {

            @Override
            public void addFormatters(FormatterRegistry registry) {
                registry.addConverter(new Converter<String, Pet>() {

                    @Override
                    public Pet convert(String source) {
                        // 啊猫,3
                        if(!StringUtils.isEmpty(source)){
                            Pet pet = new Pet();
                            String[] split = source.split(",");
                            pet.setName(split[0]);
                            pet.setAge(Integer.parseInt(split[1]));
                            return pet;
                        }
                        return null;
                    }
                });
            }

4、数据响应与内容协商

4.1 响应JSON*

将Person对象转为JSON数据返回给客户端

@RequestMapping("/test/Person")
@ResponseBody
public Person testPerson(){
    Person person = new Person("蔡徐坤", 33);
    return person;
}

4.2 SpringMVC支持哪些返回值

ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;

4.3 内容协商

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

  • 引入xml依赖
 <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
  • postman分别测试返回json和xml

只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。

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

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

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

5、视图解析与模板引擎

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

5.1 视图解析

视图解析原理流程

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

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

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

4、processDispatchResult 处理派发结果(页面改如何响应)

  • 1、render(mv, request, response); 进行页面渲染逻辑

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

视图解析:

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

5.2 模板引擎-Thymeleaf*

thymeleaf简介

现代化、服务端Java模板引擎

基本语法

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

文本值: 'one text' , 'Another one!' ,…数字: 0 , 34 , 3.0 , 12.3 ,…布尔值: true , false

空值: null

变量: one,two,.... 变量不能有空格

  • 文本操作

字符串拼接: +

变量替换: |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) ?: (defaultvalue)

  • 特殊操作

无操作: _

  • 在无标签文本中显示

[[${request.key}]]

引入页面公共部分

公共部分:

<head th:fragment="commonheader">...</div>
<div id="leftmenu" class="left-side sticky-left-side">...</div>
<div th:fragment="headermenu" class="header-section">...</div>
<div id="commonscript">...</div>

在页面引入:

<div th:include="common :: commonheader"> </div>
<div th:replace="common :: #leftmenu"></div>
<div th:replace="common :: headermenu"></div>
<div th:replace="common :: #commonscript"></div>

公共页面名 :: 需要引进的部分

设置属性值和标签值

<!--此处 th:text="${username}" 改变的是username值-->
<td th:text="${username}">username</td>

<!--此处 th:href="${baidu}" 改变的是标签中的href属性值-->
<a href="www.vip.com" th:href="${baidu}" class="btn btn-primary btn-lg" role="button">去百度</a>

迭代

<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>

<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>

条件运算

<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>

<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.3 thymeleaf使用*

  • 引入thymeleaf依赖(SpringBoot整合好的)
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 在html页面引入thymeleaf
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

原理:

SpringBoot已经整合并配置好了thymeleaf视图解析器

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

自动配好的策略

  • 1、所有thymeleaf的配置值都在 ThymeleafProperties
  • 2、配置好了 SpringTemplateEngine
  • 3、配好了 ThymeleafViewResolver
  • 4、我们只需要直接开发页面

设置好了试图前缀及视图后缀(页面需要放在templates文件夹中)

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

测试:

  • 创建视图控制器
@Controller
public class ViewController {

    @RequestMapping("/toSuccess")
    public String toSuccess(Model model){
        /*向request域中存放数据*/
        model.addAttribute("username","OdinPeng");
        model.addAttribute("age",20);
        model.addAttribute("baidu","https://www.baidu.com");
        return "success";
    }
}
  • html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head></head>
<body>

<!--此处 th:text="${username}" 改变的是username值-->
username:  <td th:text="${username}">username</td>
age:     <td th:text="${age}"></td>

<!--此处 th:href="${baidu}" 改变的是标签中的href属性值-->
<a href="www.vip.com" th:href="${baidu}">去百度</a>

</body>
</html>

5.4 简易后台管理系统

静态资源全部放到static文件夹下

  • 页面跳转
@Controller
public class IndexController {

    /*表明访问根路径"/"或"/login"时,都访问此控制器方法*/
    @GetMapping({"/","/login"})
    public String toLogin(){
        return "login";
    }

    /*登录成功后发送post请求,跳转到首页*/
    @PostMapping("/login")
    public String main(User user, HttpSession session, Model model){
        if (!user.getUserName().isEmpty() && !user.getPassword().isEmpty()){ //不可使用user.getUserName()!=null
            /*若登录成功,将用户信息保存到session域中,并重定向到toMain请求*/
            session.setAttribute("loginUser",user);
            return "redirect:/main.html";
        }else{
            /*若不成功,放回到登录界面,并将提示信息存入request域中*/
            model.addAttribute("msg","用户名或密码错误");
            return "login";
        }
    }

    /*请求跳转到主页面*/
    @GetMapping("/main.html")
    public String toMain(HttpSession session,Model model){
        /*为了防止直接访问localhost:8080/main,判断session域中是否有用户信息*/
        Object loginUser = session.getAttribute("loginUser");
        if (loginUser!=null){
            return "main";
        }else{
            model.addAttribute("msg","用户名或密码错误");
            return "login";
        }
    }
}
  • 用户信息遍历展示
@GetMapping("dynamic_table")
public String dynamic_table(Model model){
    List<User> users= Arrays.asList(
            new User("蔡徐坤","123123"),
            new User("卢本伟","321312"),
            new User("乔碧萝","123456"),
            new User("王境泽","654321"));
    model.addAttribute("users",users);
    return "table/dynamic_table";
}
        <table class="display table table-bordered" id="hidden-table-info">
        <thead>
        <tr>
            <th>#</th>
            <th>用户名</th>
            <th>密码</th>
        </tr>
        </thead>
        <tbody>
        <tr class="gradeX" th:each="user,stats:${users}"> 
            <!--stats.count表示当前用户号-->
            <td th:text="${stats.count}">Trident</td>  
            <td th:text="${user.userName}">Internet</td>
            <td >[[${user.password}]]</td>
        </tr>
        </tbody>
        </table>

6、拦截器*

6.1 HandlerInterceptor 接口

  • 实现HandlerInterceptor接口,在目标处设置拦截器
/*
配置拦截器
1、配置拦截器要拦截哪些请求
2、将这些配置放在容器中
 */
public class LoginInterceptor implements HandlerInterceptor {
    /*在目标方法执行之前*/
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        Object loginUser = session.getAttribute("loginUser");
        if (loginUser!=null){
            /*有登录的用户,放行*/
            return true;
        }else{
            /*用户未登录,拦截并跳转到登录页面*/
            request.setAttribute("msg","请先登录");
            request.getRequestDispatcher("/").forward(request,response);
            return false;
        }
    }
    /*目标方法执行完以后*/
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    /*页面渲染以后*/
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

6.2 在配置类注册拦截器

/*
将拦截器注册到容器中
指定拦截器拦截规则
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    /*添加拦截器组件*/
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") 
            /*设置拦截所有请求,包括静态资源*/
                .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); 
        /*设置放行"/"和"/login"和静态资源*/
    }
}

6.3 拦截器原理

1、根据当前请求,找到HandlerExecutionChain【可以处理请求的handler以及handler的所有 拦截器】

2、先来顺序执行 所有拦截器的 preHandle方法

  • 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
  • 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;

3、如果任何一个拦截器返回false。直接跳出不执行目标方法

4、所有拦截器都返回True。执行目标方法

5、倒序执行所有拦截器的postHandle方法。

6、前面的步骤有任何异常都会直接倒序触发 afterCompletion

7、页面成功渲染完成以后,也会倒序触发 afterCompletion

7、文件上传

7.1 文件上传实现*

  • 表单页面
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
        <input type="email" name="email">
        <input type="text" name="username">
        <input type="file" name="headerImg">
        <input type="file" name="photos">
        <button type="submit">提交</button>
</form>
  • 处理文件上传的控制器方法
/*
处理文件上传请求功能
MultipartFile: 自动封装上传的文件
*/
@SneakyThrows
@PostMapping("/upload")
public String upload(
    @RequestParam("email") String email,
    @RequestParam("username") String username, //获取请求参数
    @RequestPart("headerImg")MultipartFile headerImg, //可以获取上传的图片文件
    @RequestPart("photos") MultipartFile[] photos)
{
    /*打印日志文件*/
    log.info("email={}, username={}, headerImg={}, photos={}",email,username,headerImg.getOriginalFilename(),photos.length);

    /*将文件保存在本地目录*/
    if (!headerImg.isEmpty()){
        String originalFilename = headerImg.getOriginalFilename(); //获取原始文件名
        headerImg.transferTo(new File("D:\\Java\\data\\photos\\"+originalFilename));
    }
    if (photos.length>0){
        for (MultipartFile photo:photos){
            String originalFilename = photo.getOriginalFilename();
            photo.transferTo(new File("D:\\Java\\data\\photos\\"+originalFilename));
        }
    }
    return "main";
}

MultipartFile: 自动封装上传的文件,可以处理图片,视频,text等文件

7.2 文件上传原理

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

  • 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】

  • 原理步骤

    • 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
    • 2、参数解析器来解析请求中的文件内容封装成MultipartFile
    • 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>

FileCopyUtils。实现文件流的拷贝

8、异常处理

8.1 自定义错误页*

在templates文件下的error文件中添加错误页面

代表400系列错误和500系列错误

8.2 自定义其他异常处理

  • @ControllerAdvice+@ExceptionHandler处理全局异常
/*处理整个controller异常*/
@ControllerAdvice
@Slf4j
public class ExceptionHandler {

    /*表示为异常处理器*/
    @org.springframework.web.bind.annotation.ExceptionHandler(Exception.class)
    public String handleException(Exception exception){
        log.error("异常为: {}",exception);
        return "login";
    }
}
  • @ResponseStatus+自定义异常

  • Spring底层的异常

  • 自定义实现 HandlerExceptionResolver 处理异常

8.3 异常处理配置原理

  • ErrorMvcAutoConfiguration 自动配置异常处理规则

    • 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
      • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
    • DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
    • 容器中的组件:类型:**BasicErrorController --> id:basicErrorController(json+白页 适配响应)**
      • 处理默认 /error 路径的请求;页面响应 new ModelAndView("error", model);
    • 容器中有组件 View->id是error;(响应默认错误页)
    • 容器中放组件 BeanNameViewResolver(视图解析器);按照返回的视图名作为组件的id去容器中找View对象。
    • 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
      • 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面
    • error/404、5xx.html

8.4 异常处理步骤

1、执行目标方法,目标方法运行期间有任何异常都会被catch、而且标志当前请求结束;并且用 dispatchException

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

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

3、mv = processHandlerException;处理handler发生的异常,处理完成返回ModelAndView;

  • 1、遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】

  • 2、系统默认的 异常解析器;

    • 1、DefaultErrorAttributes先来处理异常。把异常信息保存到rrequest域,并且返回null;
    • 2、默认没有任何人能处理异常,所以异常会被抛出
      • 1、如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理
    • 2、解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。
      • 3、默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html
    • 4、模板引擎最终响应这个页面 error/500.html

9、Web原生组件注入

在SpringBoot项目中注入servlet原生的servlet程序,Filter过滤器,Listener监听器

9.1 使用ServletAPI+注解

  • 首先在总配置类SpringBootApplication上添加@ServletComponentScan注解
@SpringBootApplication
@ServletComponentScan(basePackages = "com.boot.servlet") //开启servlet组件扫描,设置扫描的路径,路径下的所有原生servlet组件都会被扫描
public class SpringBootAdminApplication {}
  • servlet原生组件
@WebServlet(urlPatterns = "/myServlet")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("hello servlet");
    }
}
  • Filter原生组件
@Slf4j
@WebFilter(urlPatterns = "/images/*") //表示拦截images下的所有请求
public class MyFilter extends HttpFilter {
    @Override
    public void init() throws ServletException {
        log.info("filter初始化完成");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("filter开始工作");
        /*放行*/
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
        log.info("filter销毁");
    }
}
  • Listener原生组件
@Slf4j
@WebListener()
public class MyListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.info("监听到项目启动");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        log.info("监听到项目销毁");
    }
}

filter和listener在springboot程序运行、拦截、销毁时,使用slf4j在控制台打印的日志文件

9.3 使用RegistrationBean

先取消掉servlet,filter,listener上的web注解

@Configuration
public class MyRegistConfig {

    @Bean
    public ServletRegistrationBean myServlet(){
        MyServlet myServlet = new MyServlet();
        return new ServletRegistrationBean(myServlet,"/my","/my02");
    }

    @Bean
    public FilterRegistrationBean myFilter(){
        MyFilter myFilter = new MyFilter();
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
        return filterRegistrationBean;
    }

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

扩展:DispatchServlet 如何注册进来

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

六、数据访问

1、引入数据库配置*

  • 导入jdbc场景依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
  • 导入mysql驱动依赖
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

可以在properties标签中更改mysql的默认驱动版本


 1.8
 
  • 添加配置项

    #导入数据库场景
    datasource:
    url: jdbc:mysql://localhost:3306/spring5 #测试所用的数据库
    username: root
    password: 502502
  • 测试

在test文件夹下的SpringBootAdminApplication测试类中进行测试

@Autowired
JdbcTemplate jdbcTemplate;

@Test
void contextLoads() {
    /*数据库查询测试*/
    Integer integer = jdbcTemplate.queryForObject("select count(*) from t_account", Integer.class);
    System.out.println(integer);
    log.info("总记录数:{}",integer);
}

2、使用Druid数据源*

  • druid官方github地址

https://github.com/alibaba/druid

  • 使用Druid数据源登录http://localhost:8080/druid/sql.html可以查看sql监控,sql防火墙,web应用,URI监控...

2.1 配置方法

  • 引入druid依赖
<!--使用druid数据源-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.17</version>
</dependency>
  • 在application.yaml文件中配置数据源
#导入数据库场景
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring5
    username: root
    password: 502502
    druid:
      #开启监控和防火墙功能
      filters: stat,wall

      #监控包下的所有组件
      aop-patterns: com.boot.*

      #配置监控页功能
      stat-view-servlet:
        enabled: true
        login-username: admin
        login-password: 502502

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

      #配置监控和防火墙的属性
      filter:
        stat:
          #设置为慢请求时间
          slow-sql-millis: 1000
          log-slow-sql: true
          enabled: true
        wall:
          enabled: true
          config:
            #不允许删表
            drop-table-allow: false

2.2 配置原理

分析自动配置

  • 扩展配置项 spring.datasource.druid
  • DruidSpringAopConfiguration.class, 监控SpringBean的;配置项: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的配置

SpringBoot配置示例

https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

配置项列表https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8

3、整合mybatis

https://github.com/mybatis

引入mybatis依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>

3.1 配置模式

  • 在application.yaml文件中配置mybatis
#关于mybatis配置:
mybatis:
  #指定sql映射文件
  mapper-locations: classpath:mybatis/mapper/*.xml
  #mybatis所有配置在configuration中
  configuration:
    #开启驼峰命名规则,可以识别下划线
    map-underscore-to-camel-case: true
  • 创建mapper映射文件(写sql语句)
<?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:命名空间,使用dao接口的全限定名称-->
<mapper namespace="com.boot.mapper.AccountMapper">

    <!--id: sql语法的标识,mybatis使用id值找到对应sql语句,id使用对应的方法名称-->
    <!--查询: resultType: 表示结果类型,使用类的全限定名称-->
    <select id="selectAccountById" resultType="com.boot.bean.Account">
        select * from account where id=#{id};
    </select>

</mapper>
  • 创建mapper接口(返回值和参数类型与mapper映射文件对应)
@Mapper
public interface AccountMapper {
    /*根据id查询*/
    public Account selectAccountById(Integer id);
}
  • 创建service层,实现mapper接口方法
@Service
public class AccountService {
    @Autowired
    AccountMapper accountMapper;

    public Account selectAccountById(Integer id){
        Account account = accountMapper.selectAccountById(id);
        return account;
    }
}
  • 在控制器方法注入service类,并测试方法
@Autowired
AccountService accountService;

/*测试selectAccountById*/
    @GetMapping("/selectAccountById")
    @ResponseBody
    public String selectAccountById(@RequestParam("id") Integer id){
        Account account = accountService.selectAccountById(id);
        return account.toString();
    }

在页面输入:localhost:8080/selectAccountById?id=1

页面显示:Account(id=1, username=CXK)

3.2 注解模式*

使用注解模式可以不用写mapper映射文件,直接在mapper接口中写上"注解(sql语句)"

  • 在application.yaml文件中配置mybatis
#关于mybatis配置:
mybatis:
  #指定sql映射文件
  mapper-locations: classpath:mybatis/mapper/*.xml
  #mybatis所有配置在configuration中
  configuration:
    #开启驼峰命名规则,可以识别下划线
    map-underscore-to-camel-case: true
  • 创建mapper接口(在接口上写sql语句)
@Mapper
public interface Account2Mapper {

    /*插入操作*/
    @Insert("insert into account2(username,email) values(#{username},#{email})")
    @Options(useGeneratedKeys = true,keyProperty = "id")
    public void insertIntoAccount2(Account2 account2);
}

@Options(useGeneratedKeys = true,keyProperty = "id") 表示自增的列名为id

  • 创建service层,实现mapper接口方法
@Service
public class Account2Service {

    @Autowired
    Account2Mapper account2Mapper;

    public void insertIntoAccount2(Account2 account2){
        account2Mapper.insertIntoAccount2(account2);
    }
}
  • 在控制器中注入service类并测试
@Autowired
Account2Service account2Service;

@PostMapping("/insertIntoAccount2")
@ResponseBody
public String insertIntoAccount2(
        @RequestParam("username") String username,
        @RequestParam("email") String email
){
    Account2 account2 = new Account2(null, username, email);
    account2Service.insertIntoAccount2(account2);
    return "success";
}
  • 需要表单提交将数据传给insertIntoAccount2请求,在页面中创建表单
<form th:action="@{/insertIntoAccount2}" method="post">
<input type="text" name="username">
<input type="email" name="email">
<button type="submit">submit</button>
</form>

表单提交后,可以在数据表中插入数据

4、整合Mybatis-plus*

官网:https://baomidou.com/

4.1 什么是Mybatis-plus

Mybatis-plus(简称 MP)是一个mybatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

可以安装mybatisX插件快速跳转mapper接口和mapper映射文件

自动配置

  • MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。mybatis-plus:xxx 就是对**mybatis-plus的定制**
  • SqlSessionFactory 自动配置好。底层是容器中默认的数据源
  • mapperLocations 自动配置好的。有默认值。**classpath*:/mapper/*/\.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下**
  • 容器中也自动配置好了 SqlSessionTemplate
  • @Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan("com.atguigu.admin.mapper") 批量扫描就行

4.2 整合Mybatis-plus

  • 引入依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

引入mybatis-plus依赖时,自动帮助引入了jdbc和mybatis依赖

  • 编写yaml文件(与整合mybatis相同)
#导入数据库场景
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springbootdata
    username: root
    password: 502502
    druid:
      #开启监控和防火墙功能
      filters: stat,wall

      #监控包下的所有组件
      aop-patterns: com.boot.*

      #配置监控页功能
      stat-view-servlet:
        enabled: true
        login-username: admin
        login-password: 502502

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

      #配置监控和防火墙的属性
      filter:
        stat:
          #设置为慢请求时间
          slow-sql-millis: 1000
          log-slow-sql: true
          enabled: true
        wall:
          enabled: true
          config:
            #不允许删表
            drop-table-allow: false
  • Bean
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
@TableName("user") //指定绑定数据库中的表名
public class User {
    @TableField(exist = false) //代表这个属性在表中不存在
    private String userName;
    @TableField(exist = false)
    private String password;

    /*以下为数据库字段*/
    private Integer id;
    private String name;
    private Integer age;
    private String email;
}

@TabaleName可将javaBean和数据库表对应

@TableField(exist=false) 表示这个属性在数据库中不存在

  • 编写mapper接口,只需继承BaseMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {

}

BaseMapper中已经写好了CRUD功能,可在泛型中指定返回类型

  • 注入mapper并测试(直接调用mapper中父类写好的方法)
@Autowired
UserMapper userMapper;

@Test
    void UserTest(){
        User user = userMapper.selectById(2);
        log.info("用户信息:{}",user);
    }

4.3 案例:分页展示及删除功能*

  • 创建数据库

  • 创建对应JavaBean
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class User {
    private Integer id;
    private String name;
    private Integer age;
    private String email;
}
  • 创建mapper接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
  • 创建service接口
@Service
public interface UserService extends IService<User> {
}
  • 创建service实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
  • 控制器注入service类,获取分页信息,保存在request域中,并编写删除功能
@Autowired
UserService userService;

@GetMapping("/toPage")
public String toPage(
        Model model,
        @RequestParam(value = "pn",defaultValue = "1") Integer pn  
        //获取当前页数,若获取不到默认为1
){
    /*查询分页信息*/
    Page<User> userPage = new Page<>(pn,2); //设置每页显示数量
    /*page可获取查询列表,总页数,总记录数*/
    Page<User> page = userService.page(userPage);
    /*将分页信息存储在request域中*/
    model.addAttribute("page",page);
    return "page";
}

/*删除用户*/
    @RequestMapping("/deleteUser")
    public String deleteUser(
            @RequestParam("id") Integer id,
            @RequestParam(value="pn",defaultValue = "1") Integer pn,
            RedirectAttributes redirectAttributes
    ){
        userService.removeById(id);
        /*重定向时携带页码值*/
        redirectAttributes.addAttribute("pn",pn);
        /*将页码值发送到toPage请求,toPage请求会获取请求参数*/
        return "redirect:/toPage";
    }

page会存储查询的用户列表,总数,总页数...

  • 编写页面
<div class="jumbotron">
        <table class="table">
            <tr>
                <td>id</td>
                <td>name</td>
                <td>age</td>
                <td>email</td>
                <td>操作</td>
            </tr>
            <tr th:each="user:${page.records}">
                <td th:text="${user.id}">id</td>
                <td th:text="${user.name}">name</td>
                <td th:text="${user.age}">age</td>
                <td th:text="${user.email}">email</td>
                <td>
                    <a th:href="'/deleteUser?id='+${user.id}+'&pn='+${page.current}" type="button" class="btn btn-default" >
                     删除</a>
                </td>
            </tr>
        </table>

        <div>
            当前页数:[[${page.current}]] 总计页数:[[${page.pages}]] 总记录数:[[${page.total}]]
        </div>

        <div>
        <li th:if="${page.hasPrevious()}">
            <a th:href="'/toPage?pn='+(${page.current}-1)">上一页</a>
        </li>

        <li th:each="pageNum:${#numbers.sequence(1, page.pages)}">
            <a th:href="'/toPage?pn='+${pageNum}" th:text="${pageNum}"></a>
        </li>

        <li th:if="${page.hasNext()}">
            <a th:href="'/toPage?pn='+(${page.current}+1)">下一页</a>
        </li>
        </div>

</div>

page.records : 相当于获取User的list集合

page.current: 获取当前页数

page.pages: 获取总页数

page.total: 获取总记录数

page.hasPrevious(): 判断页面前是否还有信息

page.hasNext(): 判断页面后是否还有信息

th:href="'/deleteUser?id='+${user.id}+'&pn='+${page.current}" 的方式既可以指定请求映射,又可以向映射文件传入参数,多个参数使用&分隔

th:each="pageNum:${#numbers.sequence(1, page.pages)}" 表示遍历的开始页数和结束页数

七、单元测试

1、JUnit5的变化

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

作为最新版本的JUnit框架,JUnit5与之前版本的Junit框架有很大的不同。由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

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

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

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

SpringBoot整合的Junit依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

注意:

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

如果需要继续兼容junit4需要自行引入vintage

<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>

2、Junit5常用注解*

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

3、断言

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

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

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

3.1 简单断言

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

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
@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());
}

3.2 数组断言

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

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

3.3 组合断言

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

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

3.4 异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。

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

}

3.5 超时断言

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

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

3.6 快速失败

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

@Test
@DisplayName("fail")
public void shouldFail() {
 fail("This should fail");
}

4、前置条件

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

@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 对象才会被执行;当条件不满足时,测试执行并不会终止。

5、嵌套测试

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。

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

6、参数化测试

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

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

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

@NullSource: 表示为参数化测试提供一个null的入参

@EnumSource: 表示为参数化测试提供一个枚举入参

@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参

@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

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

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

7、迁移指南

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

  • 注解在 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。

八、指标监控

1、SpringBoot Actuator

1.1 简介

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

1.2 使用

  • 引入依赖
<!--引入指标监控功能-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • yaml配置
#配置actuator指标监控
management:
  endpoints:
    enabled-by-default: true #默认开启所有监控端点
    web:
      exposure:
        include: '*' #以web方式暴露所有端点

1.3 可视化

2、Actuator Endpoint

2.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 执行线程转储。

如果应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:

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

最常用的Endpoint

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

2.2 Health Endpoint

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

重要的几点:

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

2.3 Metrics Endpoint

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

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

2.4 管理Endpoints

开启与禁用Endpoints

  • 默认所有的Endpoint除过shutdown都是开启的。
  • 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint..enabled = true
management:
  endpoint:
    beans:
      enabled: true
  • 或者禁用所有的Endpoint然后手动开启指定的Endpoint
management:
  endpoints:
    enabled-by-default: false
  endpoint:
    beans:
      enabled: true
    health:
      enabled: true

暴露Endpoints

支持的暴露方式

  • HTTP:默认只暴露healthinfo Endpoint
  • JMX:默认暴露所有Endpoint
  • 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
ID JMX Web
auditevents Yes No
beans Yes No
caches Yes No
conditions Yes No
configprops Yes No
env Yes No
flyway Yes No
health Yes Yes
heapdump N/A No
httptrace Yes No
info Yes Yes
integrationgraph Yes No
jolokia N/A No
logfile N/A No
loggers Yes No
liquibase Yes No
metrics Yes No
mappings Yes No
prometheus N/A No
scheduledtasks Yes No
sessions Yes No
shutdown Yes No
startup Yes No
threaddump Yes No

3、定制Endpoint

3.1 定制Health信息

@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();
    }
}
management:
    health:
      enabled: true
      show-details: always #总是显示详细信息。可显示每个模块的状态信息
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {

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

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

3.2 定制info信息

  • 编写配置文件
info:
  appName: boot-admin
  version: 2.0.1
  mavenProjectName: @project.artifactId@  #使用@@可以获取maven的pom文件值
  mavenProjectVersion: @project.version@
  • 编写InfoContributor
@Component
public class ExampleInfoContributor implements InfoContributor {

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

http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息

3.3 定制Metrics信息

  • SpringBoot支持自动适配的Metrics

  • 增加定制Metrics

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

3.4 定制Endpoint

@Component
@Endpoint(id = "container")
public class DockerEndpoint {

    @ReadOperation
    public Map getDockerInfo(){
        return Collections.singletonMap("info","docker started...");
    }

    @WriteOperation
    private void restartDocker(){
        System.out.println("docker restarted....");
    }
}

场景:开发ReadinessEndpoint来管理程序是否就绪,或者LivenessEndpoint来管理程序是否存活

当然,这个也可以直接使用 https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-kubernetes-probes

2条评论

发表回复

您的电子邮箱地址不会被公开。