SpringBoot

SpringBoot2

简介

简化Spring应用的厨师搭建和开发过程

是高层框架,底层是SpringFramework

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

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

@SpringBootApplication:主程序类,这是一个SpringBoot应用

@RestController:@Controller + @ResponseBody

优点

  • 创建独立Spring应用
  • 内嵌web服务器
  • 自动starter依赖,简化构建配置
  • 自动配置Spring以及第三方功能
  • 提供生产级别的监控、健康检查及外部化配置
  • 无代码生成、无需编写XML

缺点

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

环境要求

  • Java8及以上
  • Maven3.3及以上

背景

微服务

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

分布式

  • SpringBoot + SpringCloud

云原生

  • 原生应用如何上云:Cloud Native

特点

依赖管理

  • 父项目做依赖管理parent:可以避免多个依赖使用相同技术时出现依赖版本冲突

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <!--依赖管理--> 
    <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>

    <!--几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制-->
  • 开发导入场景启动器starter

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.3.4.RELEASE</version>
    <scope>compile</scope>
    </dependency>
  • 无需关注版本号,自动版本仲裁

    • 引入依赖默认都可以不写版本
    • 引入非版本仲裁的jar要写版本号
  • 可以修改默认版本号

    • 查看spring-boot-dependencies里面规定当前依赖的版本用的key

    • 在当前项目里面重写配置

      1
      2
      3
      <properties>
      <mysql.version>5.1.43</mysql.version>
      </properties>

自动配置

  • Tomcat

    • 引入Tomcat依赖

    • 配置Tomcat

      1
      2
      3
      4
      5
      6
      7
      <!--Spring Boot 内嵌Tomcat服务器:将Tomcat服务器作为对象运行,并将该对象交给Spring容器管理-->
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <version>2.3.4.RELEASE</version>
      <scope>compile</scope>
      </dependency>
      1
      2
      3
      # Tomcat配置,yaml版
      server:
      port: 81
      1
      2
      # Tomcat配置,properties版
      server.port=80
  • SpringMVC

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

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

    • 主程序所在包及所有子包里的组件都会被默认扫描进来

    • 无需以前的包扫描配置

    • 要改变扫描路径:

      • @SpringBootApplication(scanBasePackages=”cc.mousse”)
      • @ComponentScan
      1
      2
      3
      4
      5
      @SpringBootApplication
      // 等同于
      @SpringBootConfiguration
      @EnableAutoConfiguration
      @ComponentScan("cc.mousse.boot")
  • 各种配置拥有默认值

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

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

底层注解

引导类

@SpringBootApplication:启动程序(创建并初始化Spring容器)

  • @SpringBootConfigration
    • @Configration
  • @ComponentScan:扫描当前包所在包及其子包

组件添加

@Configuration

  • 配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
  • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
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
/**
* 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
* 2、配置类本身也是组件
* 3、proxyBeanMethods:代理bean的方法
* Full(proxyBeanMethods = true)、【保证每个@Bean方法被调用多少次返回的组件都是单实例的】
* Lite(proxyBeanMethods = false)【每个@Bean方法被调用多少次返回的组件都是新创建的】
* 组件依赖必须使用Full模式默认。其他默认是否Lite模式
*
*
*
*/
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

/**
* Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
* @return
*/
@Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
public User user01(){
User zhangsan = new User("zhangsan", 18);
//user组件依赖了Pet组件
zhangsan.setPet(tomcatPet());
return zhangsan;
}

@Bean("tom")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}

//@Configuration测试代码
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {

public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

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

//3、从容器中获取组件
Pet tom01 = run.getBean("tom", Pet.class);
Pet tom02 = run.getBean("tom", Pet.class);
System.out.println("组件:"+(tom01 == tom02));

//4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892
MyConfig bean = run.getBean(MyConfig.class);
System.out.println(bean);
//如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
//保持组件单实例
User user = bean.user01();
User user1 = bean.user01();
System.out.println(user == user1);
User user01 = run.getBean("user01", User.class);
Pet tom = run.getBean("tom", Pet.class);
System.out.println("用户的宠物:"+(user01.getPet() == tom));
}
}

@Bean/@Component/@Controller/@Service/@Repository

@ComponentScan/@Import

1
2
3
4
5
6
7
8
9
/**
* @Import({User.class, DBHelper.class})
* 给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
*/

@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
}

@Conditional

  • 条件装配:满足Conditional指定的条件,则进行组件注入
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
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
//@ConditionalOnBean(name = "tom")
@ConditionalOnMissingBean(name = "tom")
public class MyConfig {


/**
* Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象
* @return
*/

@Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
public User user01(){
User zhangsan = new User("zhangsan", 18);
//user组件依赖了Pet组件
zhangsan.setPet(tomcatPet());
return zhangsan;
}

@Bean("tom22")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}

public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);

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

boolean tom = run.containsBean("tom");
System.out.println("容器中Tom组件:"+tom);

boolean user01 = run.containsBean("user01");
System.out.println("容器中user01组件:"+user01);

boolean tom22 = run.containsBean("tom22");
System.out.println("容器中tom22组件:"+tom22);


}

配置引入

@ImportResource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
======================beans.xml=========================
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<bean id="haha" class="com.atguigu.boot.bean.User">
<property name="name" value="zhangsan"></property>
<property name="age" value="18"></property>
</bean>

<bean id="hehe" class="com.atguigu.boot.bean.Pet">
<property name="name" value="tomcat"></property>
</bean>
</beans>
1
2
3
4
5
6
7
8
@ImportResource("classpath:beans.xml")
public class MyConfig {}

======================测试=================
boolean haha = run.containsBean("haha");
boolean hehe = run.containsBean("hehe");
System.out.println("haha:"+haha);//true
System.out.println("hehe:"+hehe);//true

配置绑定

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

@ConfigurationProperties

@EnableConfigurationProperties + @ConfigurationProperties

1
2
3
4
5
@EnableConfigurationProperties(Car.class)
//1、开启Car配置绑定功能
//2、把这个Car这个组件自动注册到容器中
public class MyConfig {
}

@Component + @ConfigurationProperties

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
/**
* 只有在容器中的组件,才会拥有SpringBoot提供的强大功能
*/
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {

private String brand;
private Integer price;

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public Integer getPrice() {
return price;
}

public void setPrice(Integer price) {
this.price = price;
}

@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", price=" + price +
'}';
}
}

最佳实践

引入场景依赖:https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter

查看自动配置了哪些(选做)

  • 自己分析,引入场景对应的自动配置一般都生效了

  • 配置文件中debug=true开启自动配置报告。Negative(不生效)\Positive(生效)

是否需要修改

开发技巧

Lombok

简化JavaBean开发

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</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
// 简化JavaBean开发
@NoArgsConstructor // 无参构造器
// @AllArgsConstructor // 全参构造器
@Data // 生成get/set方法
@ToString // 生成toString方法
@EqualsAndHashCode //生成equalsAndHashCode方法
public class User {

private String name;
private Integer age;
private Pet pet;

public User(String name,Integer age){
this.name = name;
this.age = age;
}

}


// 简化日志开发
@Slf4j
@RestController
public class HelloController {

@RequestMapping("/hello")
public String handle01(@RequestParam("name") String name){

log.info("请求进来了....");

return "Hello, Spring Boot 2!"+"你好:"+name;
}

}

dev-tools

项目或者页面修改以后:Ctrl+F9

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

Spring Initailizr

项目初始化向导

resources/static:静态资源

resources/templates:页面

resourves/application.properties:配置文件

核心

配置文件

优先级

  • properties > yml > yaml
  • 属性共存叠加并相互覆盖

properties

同以前的properties用法

yaml/yml

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

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

基本语法

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

数据类型

  • 字面量:单个的、不可再分的值

    • date、boolean、string、number、null
    1
    k: v
  • 对象:键值对的集合

    • map、hash、set、object
    1
    2
    3
    4
    5
    6
    7
    #行内写法:
    k: {k1:v1,k2:v2,k3:v3}
    #或
    k:
    k1: v1
    k2: v2
    k3: v3
  • 数组:一组按次序排列的值

    • array、list、queue
    1
    2
    3
    4
    5
    6
    7
    #行内写法
    k: [v1,v2,v3]
    #或者
    k:
    - v1
    - v2
    - v3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@ConfigurationProperties("person")
@Component
@Data
public class Person {

//@Value("${person.userName}")
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> salaries;
private Map<String, List<Pet>> allPets;

}

@Component
@Data
public class Pet {

private String name;
private Double weight;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# yaml表示以上对象
person:
# 单引号会将\n作为字符串输出, 双引号会将\n作为换行输出
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [ 篮球,游泳 ]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [ 131,140,148 ]
chinese: { first: 128,second: 136 }
salarys: [ 3999,4999.98,5999.99 ]
allPets:
sick:
- { name: tom }
- { name: jerry,weight: 47 }
health: [ { name: mario,weight: 47 } ]

# 通过${属性名}方式引用属性值
string1: "hello "
string2: "${string1} world"
1
2
3
4
5
6
7
8
9
// 使用@Autowired把所有的数据封装到一个Environment中
@Autowired
private Environment env;

@GetMapping("/env")
@ResponseBody
public String getEnv() {
return env.getProperty("address.city");
}

配置提示

  • 自定义的类和配置文件绑定一般没有提示
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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
<!-- 不要打包配置提示插件 -->
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

web开发

SpringMVC

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

    • 内容协商视图解析器和BeanName视图解析器

    • 静态资源(包括webjars)

    • 自动注册 Converter,GenericConverter,Formatter

    • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)

    • 自动注册 MessageCodesResolver (国际化用)

    • 静态index.html 页支持

    • 自定义 Favicon

    • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

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

  • 声明 WebMvcRegistrations 改变默认底层组件

  • @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration全面接管SpringMVC

简单功能

静态资源访问

  • 静态资源目录

    • 放在类路径下: called /static (or /public or /resources or /META-INF/resources

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

    • 原理: 静态映射/**

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

      1
      2
      3
      4
      5
      spring:
      web:
      resources:
      static-locations:
      [ classpath:/pz/ ]
  • 静态资源访问前缀

    • 默认无前缀
    • 当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
    1
    2
    3
    4
    spring:
    mvc:
    # 会导致welcome page功能失效
    static-path-pattern: /res/**
  • webjars

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

欢迎页支持

  • 方法1:静态资源路径下index.html
    • 可以配置静态资源路径
    • 不可以配置静态资源的访问前缀,否则导致index.html不能被默认访问
  • 方法2:controller能处理/index
1
2
3
4
spring:
mvc:
# 会导致welcome page功能失效
static-path-pattern: /res/**

自定义Favicon

  • 网站图标
  • favicon.ico放在静态资源目录下即可
1
2
3
4
spring:
mvc:
# 会导致Favicon功能失效
static-path-pattern: /res/**

请求参数处理

注解与参数

  • 基本注解

    • @PathVariable:获取路径变量

      • 若有Map<String, String>:把所有的变量放进此Map中
    • @RequestHeader:获取请求头

      • 若有Map<String, String>:把所有的请求头放进此Map中
    • @RequestParam:获取请求参数

      • 若有Map<String, String>:把所有的请求头放进此Map中
    • @CookieValue:获取Cookie值

      • String:获取Cookie的值
      • Cookie:获取Cookie对象
      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
      @GetMapping("/car/{id}/owner/{username}")
      public Map<String, Object> getCar(
      // 获取路径变量
      @PathVariable Integer id,
      @PathVariable String username,
      @PathVariable Map<String, String> pathVariable,
      // 获取请求头
      @RequestHeader("User-Agent") String userAgent,
      @RequestHeader Map<String, String> headers,
      // 获取请求参数
      @RequestParam("age") Integer age,
      @RequestParam("interests") List<String> interests,
      @RequestParam Map<String, String> params,
      // 获取Cookie值
      @CookieValue("_ga") String _ga,
      // 获取Cookie对象
      @CookieValue("_ga") Cookie cookie
      ) {
      Map<String, Object> map = new HashMap<>();
      map.put("id", id);
      map.put("username", username);
      map.put("pathVariable", pathVariable);
      map.put("userAgent", userAgent);
      map.put("headers", headers);
      map.put("age", age);
      map.put("interests", interests);
      map.put("params", params);
      map.put("_ga", _ga);
      System.out.println(cookie.getName() + " " + cookie.getValue());
      return map;
      }
    • @RequestBody:获取请求体(POST)

      1
      2
      3
      4
      5
      6
      @PostMapping("/save")
      public Map<String, Object> postMethod(@RequestBody String responseBody) {
      Map<String, Object> map = new HashMap<>();
      map.put("responseBody", responseBody);
      return map;
      }
    • @RequestAttribute:获取request域属性

      • 若有HttpServletRequest:把域属性放入此HttpServletRequest中
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @ResponseBody
      @GetMapping("/success")
      public Map<String, Object> success(
      @RequestAttribute("msg") String msg,
      HttpServletRequest request
      ) {
      Map<String, Object> map = new HashMap<>();
      Object code = request.getAttribute("code");
      map.put("msg", msg);
      map.put("code", code);
      return map;
      }
    • @MatrixVariable:矩阵变量

      • 矩阵变量需要在SpringBoot中手动开启

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        @Configuration
        public class WebConfig implements WebMvcConfigurer {

        // 方式1, 需实现WebMvcConfigurer
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        // 不移除分号后面内容, 矩阵变量才能生效
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
        }

        // 方式2
        @Bean
        public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
        }
        };
        }
        }
      • 根据RFC3986的规范,矩阵变量应当绑定在路径变量中

      • 若是有多个矩阵变量,应当使用英文符号;进行分隔

      • 若是一个矩阵变量有多个值,应当使用英文符号,进行分隔或为之命名多个重复的key

      • 如:

        • /cars/sell;low=34;brand=byd,audi,yd
        • 页面开发cookie被禁用时获取session里面内容:把Cookie的值使用矩阵变量的方式进行传递
          • url重写:/abc;jsesssionid=xxxx
    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
    // /cars/sell;low=34;brand=byd,audi,yd
    @GetMapping("/cars/{path}")
    public Map<String, Object> carsSell(
    // 获取矩阵变量
    @MatrixVariable Integer low,
    @MatrixVariable List<String> brand,
    // 获取路径: sell
    @PathVariable String path
    ) {
    Map<String, Object> map = new HashMap<>();
    map.put("low", low);
    map.put("brand", brand);
    map.put("path", path);
    return map;
    }

    // /boss/1;age=20/2;age=10
    // 矩阵变量必须有url路径变量才能被解析
    @GetMapping("/boss/{bossId}/{empId}")
    public Map<String, Object> boss(
    @MatrixVariable(pathVar = "bossId", value = "age") Integer bossAge,
    @MatrixVariable(pathVar = "empId", value = "age") Integer empAge
    ) {
    Map<String, Object> map = new HashMap<>();
    map.put("bossAge", bossAge);
    map.put("empAge", empAge);
    return map;
    }
  • Servlet API

    • WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
  • 复杂参数

    • Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes(重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
  • 自定义对象参数

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

模版引擎

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

Thymeleaf

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

  • 基本语法

    • 表达式

      表达式名字 语法 用途
      变量取值 ${…} 获取请求域、session域、对象等值
      选择变量 *{…} 获取上下文对象值
      消息 #{…} 获取国际化等值
      链接 @{…} 生成链接
      片段表达式 ~{…} jsp:include 作用,引入公共页面片段
    • 字面量:文本值/数字/布尔值/空值(null)/变量(不能有空格)

    • 文本操作

      • 字符串拼接:+
      • 变量替换:|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)
    • 特殊操作

      • 无操作: _
    • 设置属性值

      • th:attr
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <!--设置单个值-->
      <form action="subscribe.html" th:attr="action=@{/subscribe}">
      <fieldset>
      <input type="text" name="email" />
      <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
      </fieldset>
      </form>

      <!--设置多个值-->
      <img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

      <!--以上两个的代替写法-->
      <input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
      <form action="subscribe.html" th:action="@{/subscribe}">
    • 迭代

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <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>
    • 条件运算

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <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>
  • 使用

    1. 引入Starter

      1
      2
      3
      4
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
    2. 自动配置好了thymeleaf

      • 所有thymeleaf的配置值都在 ThymeleafProperties

      • 配置好了SpringTemplateEngine

      • 配好了ThymeleafViewResolver

      • 我们只需要直接开发页面

        1
        2
        3
        public static final String DEFAULT_PREFIX = "classpath:/templates/";

        public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
    3. 开发

      1
      2
      3
      4
      server:
      servlet:
      # 设置项目访问路径
      context-path: /world
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      <!DOCTYPE html>
      <html lang="en" xmlns:th="http://www.thymeleaf.org">
      <head>
      <meta charset="UTF-8">
      <title>Success</title>
      <link rel="stylesheet" type="text/css" th:href="@{/css/style.css}">
      </head>
      <body>
      <h1>Success</h1>
      <h2 th:text="${msg}"></h2>
      <!-- 地址为: http://mousse.cc -->
      <h2><a th:href="${link}" th:text="${link}"></a></h2>
      <!-- 地址为: link -->
      <h2><a th:href="@{link}" th:text="${link}"></a></h2>
      </body>
      </html>

      <a href="#" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
      <img src="images/photos/user-avatar.png" alt=""/>
      <!-- thymeleaf行内写法 -->
      [[${session.loginUser.username}]]
      <span class="caret"></span>
      </a>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Controller
      public class ViewController {

      @RequestMapping("/success")
      public String success(Model model) {
      // model中的数据会被放在请求域中, 相当于request.setAttribute()
      model.addAttribute("msg", "Hello World");
      // 若只写mousse.cc, 链接地址为: localhost:8080/mousse.cc
      model.addAttribute("link", "http://mousse.cc");
      return "success";
      }

      }

构建后台管理系统

  1. 项目创建

    • thymeleaf、web-starter、devtools、lombok
  2. 静态资源处理

    • 自动配置好,我们只需要把所有静态资源放到 static 文件夹下
  3. 路径构建

    • th:action=”@{/login}”
  4. 模板抽取

    • th:insert/replace/include
  5. 页面跳转

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @PostMapping("/index")
    public String index(User user, HttpSession session, Model model) {
    if (StringUtils.hasLength(user.getUsername()) && StringUtils.hasLength(user.getPassword())) {
    // 把登录成功的用户保存起来
    session.setAttribute("loginUser", user);
    // 登录成功后重定向至main.html, 防止表单重新提交
    return "redirect:/main.html";
    } else {
    model.addAttribute("msg", "账号密码错误");
    return "login";
    }
    }
  6. 数据渲染

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @GetMapping("/dynamic_table")
    public String dynamicTable(Model model) {
    // 动态遍历表格内容
    List<User> users = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
    users.add(new User("user" + (i + 1), UUID.randomUUID().toString().substring(0, 5)));
    }
    model.addAttribute("users", users);
    return "table/dynamic_table";
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <table class="display table table-bordered table-striped" id="dynamic-table">
    <thead>
    <tr>
    <th>#</th>
    <th>用户名</th>
    <th>密码</th>
    </tr>
    </thead>
    <tbody>
    <!-- 使用thymeleaf遍历 -->
    <tr class="gradeX" th:each="user,stats:${users}">
    <!-- 获取遍历状态 -->
    <td>[[${stats.count}]]</td>
    <td th:text="${user.username}">用户名</td>
    <td th:text="${user.password}">密码</td>
    </tr>
    </tbody>
    </table>

拦截器

  1. 实现HandlerInterceptor接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 登录检查
    public class LoginInterceptor implements HandlerInterceptor {

    // 目标方法执行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 登录检查逻辑
    HttpSession session = request.getSession();
    User loginUser = (User) session.getAttribute("loginUser");
    if (loginUser != null) {
    return true;
    } else {
    // 未登录时跳转到登录页面
    request.setAttribute("msg", "请先登录");
    request.getRequestDispatcher("/login").forward(request, response);
    return false;
    }
    }

    }
  2. 配置拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Controller
    // 实现WebMvcConfigurer接口定制功能
    public class AdminWebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginInterceptor())
    // 所有请求都会被拦截, 包括静态资源
    .addPathPatterns("/**")
    // 放行登录页面
    .excludePathPatterns("/", "/login", "/index")
    // 放行静态资源
    .excludePathPatterns("/css/**", "/fonts/**", "/images/**", "/js/**");
    }

    }

文件上传

  1. 页面表单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
    <div class="form-group">
    <label for="exampleInputEmail1">邮箱</label>
    <input type="email" name="email" class="form-control" id="exampleInputEmail1"
    placeholder="Enter email">
    </div>
    <div class="form-group">
    <label for="exampleInputPassword1">名字</label>
    <input type="text" name="username" class="form-control" id="exampleInputPassword1"
    placeholder="Password">
    </div>
    <!-- 单文件上传 -->
    <div class="form-group">
    <label for="exampleInputFile">头像</label>
    <input type="file" name="profileImg" id="exampleInputFile">
    </div>
    <!-- 多文件上传 -->
    <div class="form-group">
    <label for="exampleInputFile">生活照</label>
    <input type="file" name="photos" multiple>
    </div>
    <div class="checkbox">
    <label>
    <input type="checkbox"> Check me out
    </label>
    </div>
    <button type="submit" class="btn btn-primary">提交</button>
    </form>
  2. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    @Slf4j
    @Controller
    public class FormController {

    @GetMapping("/form_layouts")
    public String formLayouts() {
    return "form/form_layouts";
    }

    @PostMapping("/upload")
    public String upload(
    @RequestParam String email,
    @RequestParam String username,
    // MultipartFile自动封装上传的文件
    @RequestPart MultipartFile profileImg,
    @RequestPart MultipartFile[] photos
    ) throws IOException {
    log.info("email: {}", email);
    log.info("username: {}", username);
    log.info("profileImgSize: {}", profileImg.getSize());
    log.info("photosCount: {}", photos.length);
    // 保存文件
    if (!profileImg.isEmpty()) {
    String originalFilename = profileImg.getOriginalFilename();
    profileImg.transferTo(new File(originalFilename));
    }
    if (photos.length > 0) {
    for (MultipartFile photo : photos) {
    String originalFilename = photo.getOriginalFilename();
    photo.transferTo(new File(originalFilename));
    }
    }
    return "main";
    }

    }

异常处理

默认规则

  • 默认情况下Spring Boot提供/error处理所有错误的映射
  • 对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息
  • 对于浏览器客户端,响应一个“whitelabel”错误视图,以HTML格式呈现相同的数据
  • 要对其进行自定义,添加View解析为error
  • 要完全替换默认行为,可以实现ErrorController并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容
  • error/下的4xx,5xx页面会被自动解析
1
2
3
4
5
6
<h3 th:text="${message}">Something went wrong.</h3>
<p class="nrml-txt" th:text="${trace}">
Why not try refreshing you page? Or you can
<a href="#">contact our support</a>
if the problem persists.
</p>

定制错误处理逻辑

  • 自定义错误页
    • error/404.html,error/5xx.html
    • 有精确的错误状态码页面就匹配精确,没有就找4xx.html,如果都没有就触发白页

原生Servlet组件

使用Servlet API

  • @ServletComponentScan(basePackages = “cc.mousse”)

    • 指定原生Servlet组件都放在哪
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @ServletComponentScan(basePackages = "cc.mousse")
    @SpringBootApplication
    public class ManagementSystemApplication {

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

    }
  • @WebServlet(urlPatterns = “/my”)

    • 效果:直接响应,没有经过Spring的拦截器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @WebServlet(urlPatterns = "/my")
    public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.getWriter().write("Hello World");
    }

    }
  • @WebFilter(urlPatterns={“/css/“,”/images/“})

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Slf4j
    // /**为Spring写法, /*为Servlet写法
    @WebFilter(urlPatterns = {"/css/*", "/images/*"})
    public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    log.info("MyFilter初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    log.info("MyFilter运作");
    // 放行
    filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
    log.info("MyFilter销毁");
    }

    }
  • @WebListener

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Slf4j
    @WebListener
    public class MyServletContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
    log.info("MyServletContextListener: 项目初始化");
    }

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

    }

使用RegistrationBean

  • ServletRegistrationBean,FilterRegistrationBean,ServletListenerRegistrationBean
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
// proxyBeanMethods = true: 保证依赖的组件始终是单实例的
@Configuration
public class MyRegistrationConfig {

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

@Bean
public FilterRegistrationBean myFilter() {
// 方式1
return new FilterRegistrationBean(new MyFilter(), myServlet());
// 方式2
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new MyFilter());
filterRegistrationBean.setUrlPatterns(Arrays.asList("/css/*", "/images/*"));
return filterRegistrationBean;
}

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

}

定制化原理

常见方式

  • 修改配置文件

  • xxxxxCustomizer

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

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

    1
    2
    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer
  • @EnableWebMvc + WebMvcConfigurer + @Bean可以全面接管SpringMVC,所有规则全部自己重新配置,实现定制和扩展功能

    1
    2
    3
    @EnableWebMvc
    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer

原理分析套路

  1. 场景starter
  2. xxxxAutoConfiguration
  3. 导入xxx组件
  4. 绑定xxxProperties
  5. 绑定配置文件项

数据访问

SQL

自动配置
  • HikariDataSource

  • 导入JDBC场景

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
    </dependency>
    • 导入数据库驱动

      • 官方不知道我们接下要操作什么数据库

      • 数据库版本和驱动版本对应

        • 若要修改版本:
          • 直接依赖引入具体版本(maven的就近依赖原则)
          • 重新声明版本(maven的属性的就近优先原则)
        1
        2
        3
        4
        5
        6
        <!-- MySQL驱动已经有版本仲裁 -->
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        </dependency>
  • 分析自动配置

    • DataSourceAutoConfiguration: 数据源的自动配置

      • 修改数据源相关的配置:spring.datasource
      • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的
      • 底层配置好的连接池是:HikariDataSource
      1
      2
      3
      4
      5
      6
      7
      @Configuration(proxyBeanMethods = false)
      @Conditional(PooledDataSourceCondition.class)
      @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
      @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
      DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
      DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
      protected static class PooledDataSourceConfiguration
    • DataSourceTransactionManagerAutoConfiguration: 事务管理器的自动配置

    • JdbcTemplateAutoConfiguration: JdbcTemplate的自动配置,可以来对数据库进行crud

      • 可以修改这个配置项@ConfigurationProperties(prefix = “spring.jdbc”) 来修改JdbcTemplate
      • @Bean@Primary JdbcTemplate;容器中有这个组件
    • JndiDataSourceAutoConfiguration: jndi的自动配置

    • XADataSourceAutoConfiguration: 分布式事务相关的

  • 修改配置项

    1
    2
    3
    4
    5
    6
    spring:
    datasource:
    url: jdbc:mysql://localhost:3306/springboot
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
1
2
3
4
5
6
@Test
void jdbcTest() {
String sql = "select count(*) from account_tbl";
Long count = jdbcTemplate.queryForObject(sql, Long.class);
log.info("account_tbl总记录数: {}", count);
}
Druid
  • druid官方github地址:https://github.com/alibaba/druid

  • 整合第三方技术的方式

    • 自定义方式

      1. 创建数据源

        1
        2
        3
        4
        5
        6
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
        <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.8</version>
        </dependency>
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        // 默认自动配置是判断容器中没有才会配置HikariDataSource
        // @ConditionalOnMissingBean(DataSource.class)
        @Bean
        @ConfigurationProperties("spring.datasource")
        public DataSource dataSource() throws SQLException {
        DruidDataSource dataSource = new DruidDataSource();
        // 加入监控, 防火墙功能, 也可以在配置文件中实现
        dataSource.setFilters("stat,wall");
        return dataSource;
        }
        1
        2
        3
        4
        5
        spring:
        datasource:
        # 设置防火墙等参数
        filters: stat,wall
        max-active: 10
      2. StatViewServlet

        • StatViewServlet的用途包括:
          • 提供监控信息展示的html页面
          • 提供监控信息的JSON API
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        // 配置Druid的监控页功能
        @Bean
        public ServletRegistrationBean<StatViewServlet> statViewServlet() {

        // 配置拦截路径
        ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        // 设置登录账户
        registrationBean.addInitParameter("loginUsername","admin");
        registrationBean.addInitParameter("loginPassword","123456");
        return registrationBean;
        }
      3. StatFilter

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

        • 系统中所有filter

          别名 Filter类名
          default com.alibaba.druid.filter.stat.StatFilter
          stat com.alibaba.druid.filter.stat.StatFilter
          mergeStat com.alibaba.druid.filter.stat.MergeStatFilter
          encoding com.alibaba.druid.filter.encoding.EncodingConvertFilter
          log4j com.alibaba.druid.filter.logging.Log4jFilter
          log4j2 com.alibaba.druid.filter.logging.Log4j2Filter
          slf4j com.alibaba.druid.filter.logging.Slf4jLogFilter
          commonlogging com.alibaba.druid.filter.logging.CommonsLogFilter
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        // WebStatFilter用于采集web-jdbc关联监控的数据
        @Bean
        public FilterRegistrationBean<WebStatFilter> webStatFilter() {
        FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(new WebStatFilter());
        // 设置拦截路径为/*
        filterRegistrationBean.setUrlPatterns(List.of("/*"));
        // 设置排除拦截的路径
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
        }
    • starter方式

      1. 引入druid-starter

        1
        2
        3
        4
        5
        <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.6</version>
        </dependency>
      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的配置
        1
        2
        3
        4
        5
        6
        7
        8
        private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
        private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
        private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
        private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
        private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
        private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
        private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
        private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
      3. 配置

        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
        spring:
        datasource:
        url: jdbc:mysql://localhost:3306/springboot
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        # 设置防火墙等参数
        # filters: stat,wall
        # max-active: 10
        druid:
        # 监控页配置
        stat-view-servlet:
        enabled: true
        login-username: admin
        login-password: pswd
        # 关闭重置按钮
        reset-enable: false
        # 监控web应用
        web-stat-filter:
        enabled: true
        url-pattern: /*
        # 配置排除路径, 默认就有不配也可
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
        # 配置防火墙等
        # filters: stat,wall
        # 监控此包下的所有内容
        aop-patterns: cc.mousse.*
        # 单独详细配置过滤器
        filter:
        stat:
        enabled: true
        # 配置慢查询
        slow-sql-millis: 1000
        log-slow-sql: true
        wall:
        enabled: true
MyBatis
  • https://github.com/mybatis

  • starter

    • 官方starter:spring-boot-starter-*
    • 第三方starter:*-spring-boot-starter
    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.0</version>
    </dependency>
  • 配置模式

    • 可以修改配置文件中mybatis开头的所有配置项

    • 配置

      1
      2
      3
      4
      5
      6
      7
      8
      mybatis:
      # 配置全局配置文件位置
      # config-location: classpath:mybatis/mybatis-3-config.xml
      # 配置SQL映射文件位置
      mapper-locations: classpath:mybatis/mapper/*.xml
      # 指定mybatis全局配置文件中的相关配置项, 它和config-location二选一
      configuration:
      map-underscore-to-camel-case: true
    • 全局配置文件:建议配置在mybatis.configuration

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE configuration
      PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-config.dtd">
      <configuration>
      <!-- <settings>-->
      <!-- <setting name="mapUnderscoreToCamelCase" value="true"/>-->
      <!-- </settings>-->
      </configuration>
  • 混合模式

    • SQL映射文件位置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      <?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="cc.mousse.managementsystem.mapper.AccountMapper">

      <!-- Account getById(Integer id) -->
      <select id="getById" resultType="cc.mousse.managementsystem.bean.Account">
      select *
      from account_tbl
      where id = #{id};
      </select>

      <insert id="addCity" keyProperty="id" useGeneratedKeys="true">
      insert into city(name, state, country)
      values (#{name}, #{state}, #{country});
      </insert>

      </mapper>
    • Mapper接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Mapper
      public interface AccountMapper {

      Account getById(Integer id);

      }

      @Mapper
      public interface CityMapper {

      void addCity(City city);

      }
  • 注解模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Mapper
    public interface CityMapper {

    @Select("select * from city where id = #{id}")
    City getById(Integer id);

    }

    @Mapper
    public interface CityMapper {

    @Select("insert into city(name, state, country) values (#{name}, #{state}, #{country})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    void addCity(City city);

    }
  • 总结

    1. 引入mybatis-starter
    2. 配置application.yaml中,指定mapper-location位置即可
    3. 编写Mapper接口并标注@Mapper注解
    4. 简单方法直接注解方式
    5. 复杂方法编写mapper.xml进行绑定映射
    6. @MapperScan(“com.atguigu.admin.mapper”) 简化,其他的接口就可以不用标注@Mapper注解
MyBatis-Plus
  • MyBatis-Plus(简称 MP)是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生

  • 建议安装MybatisX插件

  • 引入依赖

    1
    2
    3
    4
    5
    6
    <!-- 已包含jdbc和mybatis -->
    <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3.4</version>
    </dependency>
    • 自动配置
      • MybatisPlusAutoConfiguration配置类,MybatisPlusProperties配置项绑定
        • mybatis-plus:xxx就是对mybatis-plus的定制
      • SqlSessionFactory自动配置好,底层是容器中默认的数据源
      • mapperLocations自动配置好
        • 默认值:classpath*:/mapper/**/*.xml
        • 任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件(建议以后sql映射文件放在mapper下)
      • 容器中自动配置好了SqlSessionTemplate
      • @Mapper标注的接口也会被自动扫描,建议直接**@MapperScan**(“cc .mousse.mapper”) 批量扫描
    • 优点:Mapper继承BaseMapper就可以拥有crud能力
  • Mapper接口继承BasicMapper

    1
    2
    public interface UserMapper extends BaseMapper<User> {
    }
  • CRUD功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Data
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    // 若表名与类名一致则不用添加
    @TableName("user_tbl")
    public class User {

    // 登录用
    // 所有的属性都应该在数据库中, 不在数据库中的属性需要使用@TableField(exist = false): 表中不存在
    @TableField(exist = false)
    private String username;
    @TableField(exist = false)
    private String password;

    //数据库用
    private Long id;
    private String name;
    private Integer age;
    private String email;

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Configuration
    public class MyBatisConfig {

    // 分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
    // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
    paginationInnerInterceptor.setOverflow(true);
    // 设置最大单页限制数量,默认 500 条,-1 不受限制
    paginationInnerInterceptor.setMaxLimit(500L);
    interceptor.addInnerInterceptor(paginationInnerInterceptor);
    return interceptor;
    }

    }
    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
    @GetMapping("/dynamic_table")
    public String dynamicTable(
    Model model,
    @RequestParam(value = "page", defaultValue = "1") Integer id
    ) {
    // 分页查询数据
    Page<User> userPage = new Page<>(id, 2);
    // 分页查询的结果
    Page<User> page = userService.page(userPage, null);
    // page里包含所要查询的信息
    model.addAttribute("page", page);
    return "table/dynamic_table";
    }

    @GetMapping("/user/delete/{id}")
    public String deleteUser(
    @PathVariable Integer id,
    @RequestParam Integer page,
    RedirectAttributes redirectAttributes
    ) {
    userService.removeById(id);
    // 设置重定向参数
    redirectAttributes.addAttribute("page", page);
    return "redirect:/dynamic_table";
    }
    1
    2
    public interface UserMapper extends BaseMapper<User> {
    }
    1
    2
    public interface UserService extends IService<User> {
    }
    1
    2
    3
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @MapperScan("cc.mousse.managementsystem.mapper")
    @ServletComponentScan(basePackages = "cc.mousse")
    @SpringBootApplication
    public class ManagementSystemApplication {

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

    }
    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
    <table class="display table table-bordered table-striped" id="dynamic-table">
    <thead>
    <tr>
    <th>#</th>
    <th>编号</th>
    <th>用户名</th>
    <th>年龄</th>
    <th>邮箱</th>
    <th>操作</th>
    </tr>
    </thead>
    <tbody>
    <!-- 使用thymeleaf遍历 -->
    <tr class="gradeX" th:each="user,stats:${page.records}">
    <!-- 获取遍历状态 -->
    <td>[[${stats.count}]]</td>
    <td th:text="${user.id}">编号</td>
    <td th:text="${user.name}">用户名</td>
    <td th:text="${user.age}">年龄</td>
    <td th:text="${user.email}">邮箱</td>
    <td>
    <a class="btn btn-danger btn-sm" type="button"
    th:href="@{/user/delete/{id}(id=${user.id},page=${page.current})}">
    删除
    </a>
    </td>
    </tr>
    </tbody>
    </table>
    <div class="row-fluid">
    <div class="span6">
    <div class="dataTables_info" id="dynamic-table_info">
    当前第 [[${page.current}]] 页
    总计 [[${page.pages}]] 页
    共 [[${page.total}]] 记录
    </div>
    </div>
    <div class="span6">
    <div class="dataTables_paginate paging_bootstrap pagination">
    <ul>
    <li class="prev disabled"><a href="#">← 上一页</a></li>
    <li th:class="${num==page.current?'active':''}"
    th:each="num:${#numbers.sequence(1,page.pages)}">
    <a th:href="@{/dynamic_table(page=${num})}">[[${num}]]</a>
    </li>
    <li class="next"><a href="#">下一页 → </a></li>
    </ul>
    </div>
    </div>
    </div>
    </div>

NoSQL

  • Redis自动配置

    • RedisAutoConfiguration自动配置类
      • RedisProperties属性类:spring.redis.xxx是对redis的配置
    • 连接工厂是准备好的LettuceConnectionConfiguration、JedisConnectionConfiguration
    • 自动注入了RedisTemplate<Object, Object>:xxxTemplate
    • 自动注入了StringRedisTemplate:k/v都是String
      • key : value
      • 底层只要使用StringRedisTemplate、RedisTemplate就可以操作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
    redis:
    host: mousse.cc
    password: root
  • RedisTemplate与Lettuce

    1
    2
    3
    4
    5
    6
    @Test
    void redisTest() {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    ops.set("hello", "world");
    log.info("hello {}", ops.get("hello"));
    }
  • 切换至jedis

    1
    2
    3
    4
    5
    <!-- 导入jedis -->
    <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    </dependency>
    1
    2
    3
    4
    5
    6
    spring:
    # 配置Redis
    redis:
    host: mousse.cc
    password: root
    client-type: jedis
    1
    2
    3
    4
    5
    6
    7
    @Test
    void redisTest() {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    ops.set("hello", "world");
    log.info("hello {}", ops.get("hello"));
    System.out.println(redisConnectionFactory.getClass());
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class RedisUrlCountInterceptor implements HandlerInterceptor {

@Autowired
private StringRedisTemplate redisTemplate;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
// 默认每次访问当前uri就会计数+1
redisTemplate.opsForValue().increment(uri);
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
// 实现WebMvcConfigurer接口定制功能
public class AdminWebConfig implements WebMvcConfigurer {

/*
Filter: Servlet定义的原生组件, 脱离Spring也能用
Interceptor: Spring定义的接口, 可以使用Spring自动装配等功能
*/

// 需要通过自动注入获取, 否则拿不到redisTemplate
@Autowired
private RedisUrlCountInterceptor interceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
// RedisUrlCountInterceptor拦截器配置
registry.addInterceptor(interceptor)
// 所有请求都会被拦截, 包括静态资源
.addPathPatterns("/**")
// 放行静态资源
.excludePathPatterns("/css/**", "/fonts/**", "/images/**", "/js/**");
}

}
1
2
3
4
5
6
7
8
9
@GetMapping("/main.html")
public String main(Model model) {
ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
String mainCount = opsForValue.get("/main.html");
String indexCount = opsForValue.get("/index");
model.addAttribute("mainCount", mainCount);
model.addAttribute("indexCount", indexCount);
return "main_alt";
}

单元测试

JUnit5

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

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

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

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <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:表示方法是测试方法,但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
  • @ParameterizedTest:表示方法是参数化测试,下方会有详细介绍
  • @RepeatedTest:表示方法可重复执行,下方会有详细介绍
  • @DisplayName:为测试类或者测试方法设置展示名称
  • @BeforeEach:表示在每个单元测试之前执行
  • @AfterEach:表示在每个单元测试之后执行
  • @BeforeAll:表示在所有单元测试之前执行
  • @AfterAll:表示在所有单元测试之后执行
  • @Tag:表示单元测试类别,类似于JUnit4中的@Categories
  • @Disabled:表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
  • @Timeout:表示测试方法运行如果超过了指定时间将会返回错误
  • @ExtendWith:为测试类或测试方法提供扩展类引用
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
// 以SpringBoot环境运行测试
// 若不在引导类及其子包中需要@SpringBootTest(classes = 引导类.class)
@SpringBootTest
@DisplayName("JUnit5工程测试")
public class JUnit5Test {

@Test
void test() {
System.out.println("normal test");
}

@Disabled
@Test
void disabledTest() {
System.out.println("disabled");
}

@Test
@DisplayName("@DisplayName测试")
void displayNameTest() {
System.out.println("hello world");
}

@BeforeEach
void beforeEachTest() {
System.out.println("单元测试开始");
}

@AfterEach
void afterEachTest() {
System.out.println("单元测试结束");
}

// 必须是静态的
@BeforeAll
static void beforeAllTest() {
System.out.println("所有测试开始");
}

// 必须是静态的
@AfterAll
static void afterAllTest() {
System.out.println("所有测试结束");
}

@Disabled
// 超出规定时间抛出异常
@Timeout(value = 1, unit = TimeUnit.MILLISECONDS)
@Test
void timeoutTest() throws InterruptedException {
Thread.sleep(2);
}

@RepeatedTest(3)
void repeatedTestTest() {
System.out.println("repeated test");
}

}

断言

断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法检查业务逻辑返回的数据是否合理,所有的测试运行结束以后会有一个详细的测试报告

  • 简单断言

    方法 说明
    assertEquals 判断两个对象或两个原始类型是否相等
    assertNotEquals 判断两个对象或两个原始类型是否不相等
    assertSame 判断两个对象引用是否指向同一个对象
    assertNotSame 判断两个对象引用是否指向不同的对象
    assertTrue 判断给定的布尔值是否为 true
    assertFalse 判断给定的布尔值是否为 false
    assertNull 判断给定的对象引用是否为 null
    assertNotNull 判断给定的对象引用是否不为 null
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @DisplayName("简单断言测试")
    @Test
    void simpleAssertionTest() {
    int cal = cal(2, 3);
    // message: 自定义失败信息
    // 前面断言失败, 后面的代码都不运行
    assertEquals(5, cal, "业务逻辑失败");
    Object o1 = new Object();
    Object o2 = new Object();
    assertSame(o1, o2, "两个对象不一样");
    }
  • 数组断言

    1
    2
    3
    4
    5
    @Test
    @DisplayName("数组断言测试")
    public void arrayAssertionTest() {
    assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
    }
  • 组合断言

    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    @DisplayName("组合断言测试")
    public void allAssertionTest() {
    assertAll("Math",
    () -> assertEquals(2, 1 + 1, "结果不相等"),
    () -> assertTrue(1 > 0, "结果不为true")
    );
    }
  • 异常断言

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

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

    1
    2
    3
    4
    5
    @Test
    @DisplayName("快速失败断言测试")
    public void shouldAssertionFail() {
    fail("This should fail");
    }

前置条件

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

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

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

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

//assumeTrue和assumFalse确保给定的条件为true或false,不满足条件会使得测试执行终止。assumingThat的参数是表示条件的布尔值和对应的 Executable接口的实现对象。只有条件满足时,Executable对象才会被执行;当条件不满足时,测试执行并不会终止

嵌套测试

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
// 嵌套测试情况下外层Test不能驱动内层的Before/After之类的方法提前/之后运行
assertNUll(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可以驱动外层的Before/After之类的方法提前/之后运行
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

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

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

参数化测试

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

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

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

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

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

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

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

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

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

迁移指南

  • 注解在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
  • 把@RunWith、@Rule 和@ClassRule替换成@ExtendWith

指标监控

SpringBoot Actuator

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

使用

  1. 引入场景

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. 访问http://localhost:8080/actuator/**

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 配置监控: management是所有actuator的配置
    management:
    endpoints:
    # 默认开启所有监控端点
    enabled-by-default: true
    web:
    exposure:
    # 以web方式暴露所有端点
    include: '*'
  4. 测试

可视化

Actuator Endpoint

最常使用的端点

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:日志记录

Health Endpoint

  • 健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合
  • 重要的几点:
    • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
    • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
    • 可以很容易的添加自定义的健康检查机制
1
2
3
4
management:
endpoint:
health:
show-details: always

Metrics Endpoint

  • 提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到
    • 通过Metrics对接多种监控系统
    • 简化核心Metrics开发
    • 添加自定义Metrics或者扩展已有Metrics

管理Endpoints

  • 开启与禁用Endpoints

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

    • 需要开启或者禁用某个Endpoint,配置模式为:

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

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

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

定制Endpoint

定制Health信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class MyHealthIndicator extends AbstractHealthIndicator {

// 真实的检查方法
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Map<String, Object> map = new HashMap<>();
// 进行测试
if (1 == 1) {
builder.status(Status.UP);
// 信息
map.put("count", 1);
map.put("ms", 100);
} else {
builder.status(Status.OUT_OF_SERVICE);
map.put("error", "time out");
map.put("ms", 4000);
}
builder.withDetail("code", 100).withDetails(map);
}

}
1
2
3
4
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息

定制info信息

  • http://localhost:8080/actuator/info输出返回的所有info信息

  • 方式1:编写配置文件

    1
    2
    3
    4
    5
    6
    info:
    appName: boot-admin
    version: 2.3.3
    # 使用@@可以获取maven的pom文件值
    mavenProjectName: @project.artifactId@
    mavenProjectVersion: @project.version@
  • 方式2:编写InfoContributor

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Component
    public class AppInfoContributor implements InfoContributor {

    @Override
    public void contribute(Info.Builder builder) {
    builder.withDetail("msg", "hello world").withDetails(Collections.singletonMap("code", 100));
    }

    }

定制Metrics信息

  • SpringBoot支持自动适配的Metrics

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

    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
    @Service
    public class CityServiceImpl implements CityService {

    protected Counter counter;

    @Autowired
    private CityMapper cityMapper;

    public CityServiceImpl(MeterRegistry meterRegistry) {
    counter = meterRegistry.counter("cityService.addCity.count");
    }

    public City getCityById(Integer id) {
    return cityMapper.getById(id);
    }

    public City addCity(City city) {
    counter.increment();
    cityMapper.addCity(city);
    return city;
    }

    }

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

定制Endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Endpoint(id = "myservice")
public class MyServiceEndpoint {

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

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

}

高级特性

Profile功能

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

application-profile功能

  • 默认配置文件:application.yaml,任何时候都会加载

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

  • 激活指定环境

    • 配置文件激活

      1
      spring.profiles.actice=test
    • 命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha

    • 修改配置文件的任意值,命令行优先

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

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

@Profile条件装配功能

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

// ...

}

profile分组

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

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

外部化配置

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

外部配置源

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

配置文件查找位置

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

配置文件加载顺序

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

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

自定义starter

starter启动原理

  • starter-pom引入autoconfigurer包
  • autoconfigure包中配置使用 META-INF/spring.factories中 EnableAutoConfiguration的值,使得项目启动加载指定的自动配置类
  • 编写自动配置类 xxxAutoConfiguration -> xxxxProperties
    • @Configuration
    • @Conditional
    • @EnableConfigurationProperties
    • @Bean
    • ……
  • 引入starter — xxxAutoConfiguration — 容器中放入组件 —- 绑定xxxProperties —- 配置项

自定义starter

  • atguigu-hello-spring-boot-starter(启动器)
  • atguigu-hello-spring-boot-starter-autoconfigure(自动配置包)

运维

打包运行

  • package,会执行一遍maven生命周期,可能需要跳过test阶段

  • jar支持命令行启动需要依赖maven插件支持

    1
    2
    3
    4
    5
    6
    7
    8
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
  • widows

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    java –jar springboot.jar
    # 若端口被占用
    # 查询端口
    netstat -ano
    # 查询指定端口
    netstat -ano |findstr "端口号"
    # 根据进程PID查询进程名称
    tasklist |findstr "进程PID号"
    # 根据PID杀死任务
    taskkill /F /PID "进程PID号"
    # 根据进程名称杀死任务
    taskkill -f -t -im "进程名称"
  • linux

    • 基于Linux(CenterOS7)
    • 安装JDK,且版本不低于打包时使用的JDK版本
    • 安装包保存在/usr/local/自定义目录中或$HOME下
    • 其他操作参照Windows版进行
    1
    2
    3
    4
    5
    6
    # 后台启动
    nohup java -jar springboot.jar > server.log 2>&1 &
    # server.log为日志保存文件

    # 查询进程
    ps -ef | grep "java -jar"

高级配置

  • SpringBoot在开发和运行环境均支持使用临时参数修改工程配置

  • SpringBoot支持4级配置文件,应用于开发与线上环境进行配置的灵活设置

  • SpringBoot支持使用自定义配置文件的形式修改配置文件存储位置

  • 基于微服务开发时配置文件将使用配置中心进行管理

  • 临时属性设置

    1
    2
    java –jar springboot.jar –-server.port=80
    # 携带多个属性启动SpringBoot,属性间使用空格分隔
    • 属性加载优先顺序:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config

    • 开发环境

      • 方式1:带属性启动SpringBoot程序,为程序添加运行属性:Program arguments

      • 方式2:通过编程形式带参数启动SpringBoot程序,为程序添加运行参数

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        @SpringBootApplication
        public class SsmpApplication {

        public static void main(String[] args) {
        args[0] = "--server.port=80";
        SpringApplication.run(SsmpApplication.class, args);
        // 也可以不带args参数
        // SpringApplication.run(SsmpApplication.class);
        }

        }
  • 配置文件分类(优先级从高到低)

    1. file:config/application.yml
      • 工程路径config目录中配置文件:运维经理整体调控
    2. file:application.yml
      • 工程路径配置文件:运维人员配置涉密线上环境
    3. classpath:config/application.yml
      • 项目类路径config目录中配置文件:项目经理整体调控
    4. classpath:application.yml
      • 项目类路径配置文件:开发人员本机开发与测试
    • 多层级配置文件间的属性采用叠加并覆盖的形式作用于程序
  • 自定义配置文件

    • 通过启动参数加载指定文件路径下的配置文件

      1
      Program arguments:--spring.config.location=classpath:/ebank.properties
      • properties与yml文件格式均支持
    • 通过启动参数加载指定文件路径下的配置文件时可以加载多个配置

      1
      2
      Program arguments:--spring.config.location=classpath:/ebank.properties, classpath:/ebank-server.properties
      # 后面的配置文件会叠加并覆盖前面的
      • 多配置文件常用于将配置进行分类,进行独立管理,或将可选配置单独制作便于上线更新维护
    • 重要说明

      • 单服务器项目:使用自定义配置文件需求较低
      • 多服务器项目:使用自定义配置文件需求较高,将所有配置放置在一个目录中,统一管理
      • 基于SpringCloud技术,所有的服务器将不再设置配置文件,而是通过配置中心进行设定,动态加载配置信息

多环境开发

  • YAML版

    • 主配置文件中设置公共配置(全局)

    • 环境分类配置文件中常用于设置冲突属性(局部)

    • 单配置文件

      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
      #应用环境, 放公共配置
      spring:
      profiles:
      active: dev
      ---
      #设置环境, 用---分隔
      #开发环境
      spring:
      config:
      activate:
      on-profile: dev
      server:
      port: 82
      ---
      #测试环境
      spring:
      config:
      activate:
      on-profile: test
      server:
      port: 81
      ---
      #生产环境
      spring:
      config:
      activate:
      on-profile: pro
      server:
      port: 80
    • 多配置文件

      1
      2
      3
      4
      #application.yaml
      spring:
      profiles:
      active: dev
      1
      2
      3
      #application-dev.yaml
      server:
      port: 82
      1
      2
      3
      #application-test.yaml
      server:
      port: 81
      1
      2
      3
      #application-pro.yaml
      server:
      port: 80
    • 根据功能对配置文件中的信息进行拆分,并制作成独立的配置文件

      • 配置文件

        • application-devDB.yml
        • application-devRedis.yml
        • application-devMVC.yml
      • 使用include属性在激活指定环境的情况下,同时对多个环境进行加载使其生效,多个环境间使用逗号分隔

        1
        2
        3
        4
        5
        spring:
        profiles:
        active: dev
        include: devDB,devRedis,devMVC
        # 后面的配置文件会叠加并覆盖前面的]
      • 从Spring2.4版开始使用group属性替代include属性,降低了配置书写量

        1
        2
        3
        4
        5
        6
        7
        8
        #使用group属性定义多种主环境与子环境的包含关系
        spring:
        profiles:
        active: dev
        group:
        "dev": devDB,devRedis,devMVC
        "pro": proDB,proRedis,proMVC
        "test": testDB,testRedis,testMVC
  • Properties版

    • properties文件多环境配置仅支持多文件格式
    1
    2
    #主启动配置文件application.properties
    spring.profiles.active=pro
    1
    2
    #application-dev.properties
    server.port=82
    1
    2
    #application-test.properties
    server.port=81
    1
    2
    #application-pro.properties
    server.port=80
  • 多环境开发控制

    • 当Maven与SpringBoot同时对多环境进行控制时,以Mavn为主,SpringBoot使用@..@占位符读取Maven对应的配置属性值

    • 基于SpringBoot读取Maven配置属性的前提下,如果在Idea下测试 工程时pom.xml每次更新需要手动compile方可生效

    • Maven

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <!-- 设置多环境 -->
      <profiles>
      <profile>
      <id>env_dev</id>
      <!-- 配置属性, 配置文件会读取此变量 -->
      <properties>
      <profile.active>dev</profile.active>
      </properties>
      <!-- 设定活动环境 -->
      <activation>
      <activeByDefault>true</activeByDefault>
      </activation>
      </profile>

      <profile>
      <id>env_pro</id>
      <properties>
      <profile.active>pro</profile.active>
      </properties>
      </profile>
      </profiles>
    • SpringBoot

      1
      2
      3
      spring:
      profiles:
      active: @profile.active@

日志

  • 日志基础

    • 作用

      • 编程期调试代码
      • 运营期记录信息
        • 记录日常运营重要信息(峰值流量、平均响应时长)
        • 记录应用报错信息(错误堆栈)
        • 记录运维过程数据(扩容、宕机、报警)
    • 级别

      • TRACE:运行堆栈信息,使用率低

      • DEBUG:程序员调试代码使用

      • INFO:记录运维过程数据

      • WARN:记录运维过程报警数据

      • ERROR:记录错误堆栈信息

      • FATAL:灾难信息,合并计入ERROR

      • 启动默认为INFO级别,INFO以下的看不到

        • 方法1:Program argument:–debug

        • 方法2:application.yaml

          1
          debug: true
    • 添加日志记录操作

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      /**
      * 设置同一的返回值结果类型便于前端开发读取数据
      */
      @RestController
      @RequestMapping("/books")
      public class BookController {

      // 创建记录日志的对象
      private static final Logger log = LoggerFactory.getLogger(BookController.class);

      @Autowired
      private IBookService bookService;

      @GetMapping("/{id}")
      public Result getById(@PathVariable Integer id) {
      log.debug("debug");
      log.info("info");
      log.warn("warn");
      log.error("error");
      return new Result(true, bookService.getById(id));
      }

      }
    • 设置日志输出级别

      1
      2
      3
      4
      5
      6
      7
      # 开启debug模式,输出调试信息,常用于检查系统运行状况 
      debug: true

      # 设置日志级别,root表示根节点,即整体应用日志级别
      logging:
      level:
      root: debug
    • 设置日志组,控制指定包对应的日志输出级别,也可以直接控制指定包对应的日志输出级别

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      logging:
      # 设置日志组
      group:
      # 自定义组名,设置当前组中所包含的包
      ebank: com.itheima.controller
      level:
      root: warn
      # 为对应组设置日志级别
      ebank: debug
      # 为对包设置日志级别
      com.itheima.controller: debug
    • 使用lombok提供的注解@Slf4j简化开发,减少日志对象的声明操作

      1
      @Slf4j
  • 日志输出格式控制

    • 默认格式

      1
      2
      3
      时间 级别 PID ---	[所属线程] 所属类/接口名 : 日志信息
      # PID: 进程ID,用于表明当前操作所处的进程,当多服务同时记录日志时,该值可用于协助程序员调试程序
      # 所属类/接口名: 当前显示信息为SpringBoot重写后的信息,名称过长时,简化包名书写为首字母,甚至直接删除
    • 格式控制

      1
      2
      3
      4
      5
      6
      7
      logging:
      pattern:
      console: "%d - %m%n"
      # %d:日期 %m:消息 %n:换行
      logging:
      pattern:
      console: "%d %clr(%p) --- [%16t] %clr(%-40.40c){cyan} : %m %n"
  • 日志文件

    1
    2
    3
    4
    5
    6
    7
    8
    logging:
    file:
    # 设置日志文件
    name: server.log
    logback:
    rollingpolicy:
    max-file-size: 3KB
    file-name-pattern: server.%d{yyyy-MM-dd}.%i.log

SpringBoot
http://docs.mousse.cc/SpringBoot/
作者
Mocha Mousse
发布于
2025年5月26日
许可协议