SpringBoot

SpringBoot笔记持续更新中!

SpringBoot

发布者订阅者

样例场景: 我们需要在用户注册后 给订阅的用户推送消息(例如前100名注册的 系统自动发放徽章等)

1
2
3
4
5
6
7
8
@Override
@Transactional // 保证事件和用户注册的一致性
public Long register(User insert) {
boolean save = userDao.save(insert);
// 用户注册的事件 -> 谁订阅就给谁发送通知 (1.Mq 2.ApplicationEventPublisher)
applicationEventPublisher.publishEvent(new UserRegisterEvent(this, insert)); // this事件订阅者,发送端
return insert.getId();
}

这个时候我们可以采用Spring的推送事件ApplicationEventPublisher, 其中除此之外我们还可以采用MQ进行同等操作

  • 消息发布者 Event事件

我们定义一个事件, 继承ApplicationEvent然后定义构造函数, 我们此时场景需要用户的信息, 故我们在构造函数传入User即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @className: UserRegisterEvent
* @author: Calyee
* @description: 用户注册事件
* @version: 1.0
*/
@Getter
public class UserRegisterEvent extends ApplicationEvent {
private User user;

public UserRegisterEvent(Object source, User user) {
super(source);
this.user = user;
}
}
  • 消息接收者

此项我们只需要在方法上使用注解@EventListener或 带事务的TransactionalEventListener, 需不需要带事务则看具体应用场景, 此时的事务类型为枚举项, 事务为是否加入父事务执行, @Async异步执行

我们需要在注解处标明刚刚定义的事件推送者, 然后在方法处传入该事件, 对于 Object source则传入this事件即可, 然后对其进行处理

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
/**
* @className: UserRegisterListener
* @author: Calyee
* @description: 用户注册监听
* @version: 1.0
*/
@Component
public class UserRegisterListener {
@Autowired
private IUserBackpackService userBackpackService;
@Autowired
private UserDao userDao;
/**
* 发送改名卡
*
* @param userRegisterEvent 注册事件
*/
@Async
@TransactionalEventListener(classes = UserRegisterEvent.class, phase = TransactionPhase.AFTER_COMMIT) // 事务提交后
public void sendCard(UserRegisterEvent userRegisterEvent) {
User user = userRegisterEvent.getUser();
userBackpackService.acquireItem(user.getId(), ItemEnum.MODIFY_NAME_CARD.getId(), IdempotentEnum.UID, user.getId().toString());
}
/**
* 发送 前10名/100名 注册的徽章
*/
@Async
@TransactionalEventListener(classes = UserRegisterEvent.class, phase = TransactionPhase.AFTER_COMMIT)
public void sendBadge(UserRegisterEvent userRegisterEvent) {
User user = userRegisterEvent.getUser();
int registeredUserCount = userDao.count();
if(registeredUserCount < 10){
userBackpackService.acquireItem(user.getId(), ItemEnum.REG_TOP10_BADGE.getId(), IdempotentEnum.UID, user.getId().toString());
}else if(registeredUserCount <100){
userBackpackService.acquireItem(user.getId(), ItemEnum.REG_TOP100_BADGE.getId(), IdempotentEnum.UID, user.getId().toString());
}
}
}

对于文件结构定义: 我们可以采用定义一个event包+event包下的listener包,

配置文件

读取YML配置文件信息

导入依赖:

1
2
3
4
5
6
7
8
9
10
11
<!-- 配置文件处理器: 是SpringBoot处理yml,yaml,properties配置文件 -->
<!--
1. 以流的方式读取 application.yml
2. 以反射的方式 将application.yml的值 存储到对象中(setXxx()方法)
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<!-- 可选,它将选择权交给上级应用 -->
<optional>true</optional>
</dependency>

yml样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
product:
pname: apple
price: 20.5
is-used: true
man-date: 2021/09/09

attributes: {'color': 'red','type':'good'}
address:
province: 湖南省
city: 长沙
types:
- 水果
- 零食

对应的实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data     // Get Set
// 当processor读取到application.yml配置文件以product,存储到当前对象中
@ConfigurationProperties(prefix = "product")
@Component // IOC
public class Product implements Serializable {

private String pname;
private Double price;
private Boolean isUsed; // is-used
private Date manDate; // man-date
private Map<String, Object> attributes;
private Address address;
private List<String> types;
}
1
2
3
4
5
@Data
public class Address implements Serializable {
private String province;
private String city;
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)
@SpringBootTest()
public class ProductTest {
@Autowired
private Product product;
@Test
public void testProduct() {
Assert.assertNotNull(product);
Assert.assertEquals("apple", product.getPname());
}
}

其中我们需要导入的junit的包是这个 import org.junit.Test;

不是 import org.junit.jupiter.api.Test;

不然会报错 Runner org.junit.internal.runners.ErrorReportingRunner does not support filtering and will ther

读取其他配置文件内容

1.读取例如properties配置 @PropertySource()

在配置文件中加入注解

1
2
// 1.读取例如properties配置  并以key value存储
@PropertySource(value = {"classpath:db.properties"})// value可以加多个

db.properties配置文件

1
2
username=root
password=123456

如何在Spring中使用呢? 使用@Value(${username})注解

1
2
@Value(${username})
private String username; // root

2.读取例如xml配置 @ImportResource

在配置文件中加入注解

1
2
// 2.读取spring.xml配置文件
@ImportResource(locations = {"classpath:spring.xml"})
1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="us" class="com.hang.springboot.entity.User">
<property name="name" value="zhang"> </property>
</bean>
</beans>

读取:

1
2
3
@Autowired
@Qualifier(value = "us") // 根据bean的唯一id获取,因为在User类里面加了@Component注解,已经注入过了(id=user)
private User user;

在运行时修改配置文件信息

  1. 在命令行参数中配置

    1
    java -jar xxx.jar --server.port=9090

    此项使用必须建立在启动类的run方法中,传入了main函数中的args参数

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    public class Application {
    // 命令行参数配置(传给SpringBoot的) --server.port=xxx
    public static void main(String[] args) {
    // 1. 传入参数的启动类,可以读取命令行配置
    SpringApplication.run(Application.class, args);
    }
    }

    2.传入虚拟机系统属性

1
java -Dserver.port=9090 -jar xxx.jar

-D 设置虚拟参数 其中server.port是自己需要修改的配置

1
2
3
4
5
6
7
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// 没有传入参数
SpringApplication.run(Application.class);
}
}

在最后,我们还能添加属性在配置文件里面,例如我需要加属性: name=qiuqiu

eg: java -jar xxx.jar --server.port=9090 --name=qiuqiu

java -Dserver.port=9090 -Dname=qiuqiu -jar xxx.jar

SpringBoot读取配置信息、环境变量

  1. Environment基本的环境变量
1
2
3
// 读取环境变量信息,spring启动时,自动加载
@Autowired
private Environment environment;

​ 2.ConfigurableEnvironment环境变量

1
2
@Resource
private ConfigurableEnvironment configurableEnvironment;

ConfigurableEnvironment是Environment的子接口,功能更多

注入之后,直接调用即可

多环境配置 多数据源配置

多环境:

开发时分为多个环境: 开发环境 (dev) , 测试环境 (test) , 生产环境 (prod)

系统默认application.yml

1.通过在application.xml默认配置文件中配置

1
2
3
4
# 激活指定使用哪个环境配置文件
spring:
profiles:
active: dev

2.通过注解配置 @Profile("dev"),加入在带@Configuration注解的配置类中

多环境配置,如果设置的环境跟默认环境有相同的配置 , 则当前设置的环境会覆盖默认环境的相同配置属性

多数据源:

@Configuration修饰的配置类中配置数据源

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
@Value("${driverClassName}")
private String driverName;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Value("${url}")
private String url;

@Bean
// 表示指定了 spring.profiles.active: test的时候用这个数据源
@Profile("test") // 测试数据源
public DataSource localDataSource(){
DriverManagerDataSource source =
new DriverManagerDataSource();
source.setDriverClassName(driverName);
source.setUsername(username);
source.setPassword(password);
source.setUrl(url);
return source;
}
@Bean
// 表示指定了 spring.profiles.active: prod的时候用这个数据源
@Profile("prod")
public DataSource localDataSource(){
DruidDataSource source =
new DruidDataSource();
source.setDriverClassName(driverName);
source.setUsername(username);
source.setPassword(password);
source.setUrl(url);
return source;
}

此时则只会有一个数据源的bean

日志配置

基础配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 日志配置
logging:
# 日志级别
level:
# 根日志配置 日志级别
root: debug
# 配置该路径下的日志级别
org.springframework: error
org.apache: error
# 日志输出
file:
# name和path只能设置一个,默认输出在当前模块的根目录
name: spring.log
path: logs/

滚动配置

暂无

Pom文件相关

spring-boot-start-test

对于spring-boot-starter-test的依赖, 默认是引入junit4和junit5

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!--排除junit4-->
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

docker打包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.13</version>
<executions>
<execution>
<id>default</id>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
<configuration>
<repository>javastack/${project.name}</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
<dockerfile>Dockerfile</dockerfile>
</configuration>
</plugin>

测试类

一、普通测试类 和 套件

例如此时我有一个service类需要测试:

1
2
3
4
5
6
@Component
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
  1. 首先需要加注解@Component 交给Spring管理
  2. 然后在当前的包中右键,点击Go To -> Test -> Create New Test 一般点击OK即可
  3. 然后idea在test包下就会自动创建一个跟当前类同样结构的测试类
  4. 在测试类中需要引入注解
1
2
3
// 获取springboot的启动类
@SpringBootTest(classes = SpringBootApplication.class)
@RunWith(SpringRunner.class)

测试(单方法,单类):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest() // 获取springboot的启动类
@RunWith(SpringRunner.class)
public class CalculatorTest {

// spring支持IOC: 控制反转 再进行DI注入
@Autowired
private Calculator calculator;

@Test
void add() {
int add = calculator.add(1, 2);
// 传统sout输出 不推荐
// 推荐 断言
// 优点: 与预期结果一致,绿 不一致:红 便于捕捉没通过预期的方法
// System.out.println("add = " + add);
Assert.assertEquals(3, add); // 绿
}
}

现在需求是: 假如有多个类,类里面有很多方法需要测试,那该怎么实现一起自动化测试?

: 使用SpringBoot的套件测试 , 顾名思义:把测试方法类全部放一起测试,就跟一个整体套件一样

启动当前类就可以了

1
2
3
4
5
6
7
8
9
10
/**
* 当前方法是测试套件
* 就是把CalculatorTest 和 OtherTest
* 两个类里面的方法 全部一起测试,假如用了断言 测试完成后
* 通过与未通过一目了然
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({CalculatorTest.class,OtherTest.class}) // 测试指定类
public class SuiteTest {
}

二、其他的注解:

1) @Disabled

:禁用测试方法, 用于当前方法现在不需要进行测试

1
2
3
4
5
@Test
@Disabled("This test is not ready yet.")
public void disabledTest() {
// 没有写完的逻辑 或是 已经不需要测试的逻辑
}

2) @TestMethodOrder@Order

:配置测试方法的执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderTest {

@Order(1)
@Test
public void testMethod1() {
// 在这里写测试逻辑
}

@Order(2)
@Test
public void testMethod2() {
// 在这里写测试逻辑
}
}

3) @BeforeAll@AfterAll

:在测试类的所有测试方法前和后执行一次,可用于全局初始化和销毁资源

1
2
3
4
5
6
7
8
9
@BeforeAll
public static void initOperate() {
// 初始化操作
}

@AfterAll
public static void destoryOperate() {
// 资源销毁操作
}

4) @BeforeEach@AfterEach

:在测试类的每个测试方法前和后都会执行一次

1
2
3
4
5
6
7
8
9
@BeforeEach
public void beforeEachTest() {
// 执行前的准备工作
}

@AfterEach
public void afterEachTest() {
// 执行后的清理工作
}

5) @RepeatedTest

:指定测试方法重复执行

1
2
3
4
@RepeatedTest(6)
public void repeatedTest() {
// 该测试方法会重复执行6次
}

6) @ParameterizedTest@ValueSource

:用于参数化测试

1
2
3
4
5
@ParameterizedTest
@ValueSource(strings = { "name1", "name2", "name3" })
public void testParameter(String name) {
// 使用参数化的名称进行测试
}

7) @AutoConfigureMockMvc

:启用MockMvc的自动配置,可用于测试接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
@AutoConfigureMockMvc
public class MyControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Test
public void testController() throws Exception {
mockMvc.perform(get("/api/someendpoint"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
}

切换服务器

在pom.xml里面修改,例如需要把tomcat修改为undertow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 1. 从starter-web中排除tomcat(原来的服务器)依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 2. 加入新的服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

部分执行控制台输出如下

定时器

  1. 在启动类中标明注解@EnableScheduling
  2. 编写一个Job任务类,在类上面标明@Component注解给spring托管
  3. 在@Scheduled中写cron表达式设置定时

以Redis缓存文章浏览量同步数据库为例: (当前需要在启动时,需要有缓存,需要看下面的启动任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class UpdateViewCountJob {
@Autowired
private RedisCache redisCache;
@Autowired
private ArticleService articleService;
@Scheduled(cron = "0 */10 * * * ?") // 开启定时任务
public void updateViewCountJob(){
// 获取redis中存取的浏览量
Map<String, Integer> viewCountMap = redisCache.getCacheMap(SystemConstants.ARTICLE_VIEW_COUNT);
// 双列集合不能用流,可以先转entrySet(键值对)或者keySet
List<Article> articleList = viewCountMap.entrySet()
.stream() // key(long) value(long)
.map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
.collect(Collectors.toList());
// 更新到数据库
articleService.updateBatchById(articleList);
}
}

启动前预加载服务

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
/**
* @ClassName ViewCountRunner
* @Description 在启动之前对浏览量进行查询 然后缓存到redis里面
*/
@Component
public class ViewCountRunner implements CommandLineRunner {
@Autowired
private ArticleMapper articleMapper;
@Autowired
private RedisCache redisCache;
@Override
public void run(String... args) throws Exception {
// 查询博客信息 id + viewCount key value
List<Article> articleList = articleMapper.selectList(null);
Map<String, Integer> map = new HashMap<>();
// articleList.forEach(item->{
// map.put(String.valueOf(item.getId()),item.getViewCount());
// });
map = articleList.stream()
.collect(Collectors.toMap(article -> article.getId().toString(), // key
article -> { // value
return article.getViewCount().intValue(); // 1L 在redis中要用Integer
}));
// 存储到redis
redisCache.setCacheMap(SystemConstants.ARTICLE_VIEW_COUNT,map);
}
}

自定义的Runner实现了CommandLineRunner类,重写run方法,该方法在SpringBoot启动时也会同步执行该任务,这个是SpringBoot提供的简单类

: 如果在上下文中,存在多个该接口实现类,可以通过@order注解,指定加载顺序

@Order写在实现类上 例如@Order(value = 1) 越前越快执行


SpringBoot的自动配置

1、了解注解及一些类

@Configuration结合@Bean

eg. @Configuration,里面的proxyBeanMethods属性 true : false

proxyBeanMethods配置类是用来指定@Bean注解标注的方法是否使用代理,默认是true使用动态代理,直接从IOC容器之中取得对象;
如果设置为false,也就是不使用注解,每次调用@Bean标注的方法获取到的对象和IOC容器中的都不一样,是一个新的对象,所以我们可以将此属性设置为false来提高性能,但是不能扩展,例如AOP切面

优缺点: 设置true动态代理的可以后期被切面增强,但是启动速度性能慢些,设置false则使用new对象,提高性能但是不能被增强

@EnableAutoConfiguration(xx.class)

帮助SpringBoot应用将所有符合条件@Configuration配置都加载到当前SpringBoot,并创建对应配置类的Bean,并把该Bean实体交给IoC容器进行管理

该注解的功能更强大,托管配置类并把bean托管,可以读取属性文件注入

@ConfigurationProperties(prefix = "")

一次性读取符合前缀的属性配置文件,只能注入属性

@Import
  1. 导入外部的class文件

    1
    2
    3
    @Import({Apple.class,Product.class}) // 导入两个class
    // 此时的实例化注入对象,是这种形式的 例如 Apple类原来在外部文件中 org.friut中
    // 那么由该方法实例化的对象的id是这样的 "org.friut.Apple"

    2.导入一个包的多个类

假如需要导入的类多了,那么则需要使用该方式,我们需要创建一个类实现ImportSelector接口

1
2
3
4
5
6
7
8
9
10
11
public class ImportList implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
String url = "META-INF/"
// TODO: File file = new File(url); // 将所有需要的全类名 封装到一个固定的文件内 在依次读取
// file.listFile();
return new String[]{
// 把扫描到的文件名添加到此处
};
}
}

然后使用@Import({ImportList.class,Other.class})导入刚刚的类

ApplicationContextAware接口在类中注入Spring容器对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class SpringBeansGetInfoClass implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
// 读取beanName
@GetMapping("/getSpringBeanInfo")
public void getBeanNameInfo(){
for (String name : this.context.getBeanDefinitionNames()) {
System.out.println(name);
}
}
}

实现ApplicationContextAware接口的类必须被Spring所管理

作用: 换句话说,就是这个类可以直接获取Spring配置文件中,所有有引用到的Bean对象

@Conditional

条件判断注解

例如,我需要动态加载Tomcat或Jetty服务器,即 如果容器中有服务器某Bean则加载,当然还要判断是否有多个服务器判断(当前实例未做判断,具体看自动配置WebServer服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 这个是一个WebServer自动配置类
* 该案例为动态切换Tomcat和Jetty服务器的实例配置类
*/
@Configuration // 交给Spring管理
public class WebServerAutoConfiguration {
// Spring配置文件
// WebServer自动配置类
@Bean
// 条件加载,根据TomcatCondition中约束的条件,动态注入Bean
@Conditional(TomcatCondition.class)
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
@Conditional(JettyCondition.class) // Jetty服务器条件加载判断
public JettyWebServer jettyWebServer() {
return new JettyWebServer();
}
}

条件判断类 必须实现Condition接口,重写matches方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TomcatCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
// 判断项目中是否有符合条件的依赖
try {
// 利用类加载器,加载需要判断的类
conditionContext.getClassLoader().loadClass("org.apache.catalina.startup.Tomcat");
return true;// 能走到这个地方 证明有
} catch (ClassNotFoundException e) {
return false; // 没有该类
}
// // 如果返回true,则符合条件
// return false;
}
}

2、自动配置start

样例一: DataSource

以读取spring.datasource为例:

例如我的application.yml如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8080

spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
serialization:
write-dates-as-timestamps: false

查看源码

我们简单的查看到,该类里面包含一些通用的数据源属性,并且通过注解@ConfigurationProperties(prefix = "spring.datasource")读取application.yml配置文件设置的值, 但是注意到,该属性在此处并未在spring容器中注入,那么spring是如何进行初始化的呢?

当前是自动装配,那当然是自动装配了,那么是在哪儿自动装配?

: 在此包中 , 很容易找到一个名为DatasourceAutoConfiguration的类 ,通过类名注意到这很显然是一个自动装配数据源的类

找到自动配置类, 即 带@EnableConfigurationProperties注解的类, 通过此注解对刚刚已经注入属性的类进行IOC注册, 那么这个就被自动装配了


样例二: WebServer

在此处,再举一个例子(样例二)如下,例如我需要对服务器的配置 , 比如server.port=9090服务器的端口等

步骤一: 对服务器的基础配置进行读取

文件在org.springframework.bootspring-boot-autoconfigure里面的org.springframework.boot.autoconfigure.web.ServerProperties

ServerProperties类 里面同样可以看到注解@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
@ConfigurationProperties(
prefix = "server", // 读取配置文件属性
ignoreUnknownFields = true
)
public class ServerProperties {
private Integer port;
private InetAddress address;
@NestedConfigurationProperty
private final ErrorProperties error = new ErrorProperties();
private ForwardHeadersStrategy forwardHeadersStrategy;
private String serverHeader;
private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8L);
private Shutdown shutdown;
@NestedConfigurationProperty
private Ssl ssl;
@NestedConfigurationProperty
private final Compression compression;
@NestedConfigurationProperty
private final Http2 http2;
private final Servlet servlet;
private final Reactive reactive;
private final Tomcat tomcat;
private final Jetty jetty;
private final Netty netty;
private final Undertow undertow;
// 方法省略
}

步骤二: 对服务器配置完成的类在IOC容器中进行注册bean

在当前的package下的

org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration

我们可以看到熟悉的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@AutoConfiguration
@ConditionalOnWebApplication // 注册ServerProperties配置类
@EnableConfigurationProperties({ServerProperties.class}) // 主要注解
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
public EmbeddedWebServerFactoryCustomizerAutoConfiguration() {
}

@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({HttpServer.class})
public static class NettyWebServerFactoryCustomizerConfiguration {
public NettyWebServerFactoryCustomizerConfiguration() {
}

@Bean
public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties) {
return new NettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
// 以下的省略
}
两个样例总结

从以上步骤可以看出spring-boot-starter的配置流程较简单,简化一下流程即为:

1.添加@ConfigurationProperties读取application.yml配置文件
2.配置@EnableAutoConfiguration注解类,自动扫描package生成所需bean
3.添加spring.factories配置让spring-boot-autoconfigure对当前项目进行AutoConfiguration

SpringBoot自动配置

样例总结 说到读取spring.factories,那么springboot底层是如何进行读取,并且自动配置呢?

首先我们从启动类入口入手,@SpringBootApplication , 追踪发现注解@EnableAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration // 自动装配
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};
// 略
}

打开@EnableAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}

它导入了一个名为AutoConfigurationImportSelector的自动装配类选择器 , 并且追踪, 它实现了DeferredImportSelector 该类为 ImportSelector的子类

1
2
3
4
5
6
7
8
9
10
11
12
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();
private static final String[] NO_IMPORTS = new String[0];
private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class);
private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
private ConfigurableListableBeanFactory beanFactory;
private Environment environment;
private ClassLoader beanClassLoader;
private ResourceLoader resourceLoader;
private ConfigurationClassFilter configurationClassFilter;
// 此处方法构造器省略
}

ImportSelector类,实现该类重写方法获取需要扫描的包 , 并且返回全类名

1
2
3
4
5
6
7
8
public interface ImportSelector {
// 重写该方法
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}

在AutoConfigurationImportSelector中实现了selectImports方法

1
2
3
4
5
6
7
8
9
10
11
// 返回一个字符串数组,保存扫描类的路径
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationEntry autoConfigurationEntry =
// 通过该方法(getAutoConfigurationEntry)获取自动配置的项
this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}

getAutoConfigurationEntry方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations =
// 获取候选的配置属性 (点进方法)
this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
}

getCandidateConfigurations方法 获取候选的配置属性

1
2
3
4
5
6
7
8
9
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new
// 加载工厂 bean -> loadFactoryNames()
ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
// 读取第二个配置文件 -> load()
ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}

很容易发现 , springboot使用spring的类加载器,加载类 , 在断言处,发现不能为空的断言,分析: 没有自动配置类找到在路径META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中,也就是说 读取配置类是读取固定路径(这种机制叫做SPI机制),是一种服务提供发现机制, 然后读取了两个文件

其中这两个文件,包含了大量SpringBoot启动时需要读取的配置类

打开加载工厂bean方法 loadFactoryNames (该方法读取的是第一个配置文件 spring.factories)

通过类加载器,通过文件配置

读取第二个配置文件 通过 load()方法,点开发现

通过流的读取,把配置文件按行读取,形成一个数组

在第二个配置文件中读取到例如org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration的Web服务器类,spring则会去自动读取扫描配置Web服务的配置类并且注入托管,就是前面说到的[Web配置流程](#样例二: WebServer) ,经历 配置文件读取,配置类读取,配置类根据条件注入,bean托管等流程

SpringBoot Starter分析及自定义

SpringBoot Starter: 启动类 分析starter的启动流程:

依赖分析: 从SpringBoot starter开始
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

追踪发现 包含依赖:

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

再追踪: 然后发现了有一个依赖是spring-boot-autoconfigure

1
2
3
4
5
6
7
<dependency>
<groupId>org.springframework.boot</groupId>
<!--自动配置依赖-->
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.7.3</version>
<scope>compile</scope>
</dependency>

打开依赖 org.springframework.boot:spring-boot-starter

idea 中的 maven依赖结构如下:

spring-boot-starter

​ META-INF

​ LICENSE.txt

​ MANIFEST.MF

​ NOTICE.txt

所以:其实这个starter最重要的就是pom.xml文件,它引入了autoconfiguration的jar包

需要打开本地加载的依赖文件才能看到具体的结构

依赖结构:

标准的SpringBoot是将所有的自动配置类都写在了 xxx.factories,但我们自定义的starter是将配置类都写在了spring.factories文件中

MyBatis Starter

结构如下

其中mybatisspring.factories的内容如下:

1
2
3
4
5
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

##### 两个案例总结:

包含两个模块:

一个是```starter```,一个是```autoconfiguration```

其中在starter里面主要就是**引入autoconfiguration** 以及帮我们版本仲裁其它jar包

在```autoconfiguration```中是**托管配置类** , 比如我当前开发的Jdbc模块:那么我的starter则需要导入一些依赖,**重点是导入我的JdbcAutoConfiguration配置类**,其中它需要读取一些属性配置文件```Properties```,它包含了当前模块的配置属性信息, 位于```META-INF/spring.factories```的配置文件则是SpringBoot会自动读取的配置文件

```java
# Auto Configure 使用SpringBoot自动配置类 读取配置类 INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hang.myAutoConfiguration.JdbcAutoConfiguration

其中start中的pom文件依赖了autoconfiguration

1
2
3
4
5
6
7
<!-- starter只做了这一件事情:
引入AutoConfiguration配置类-->
<dependency>
<groupId>com.hang</groupId>
<artifactId>JdbcAutoConfiguration</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

至此: 我们只需要在SpringBoot项目中引入自定义的starter即可

自定义starter

以日期格式化DateFormat为例

  1. 定义一个autoconfiguration模块(包含AutoConfiguration和Properties)

AutoConfiguration

1
2
3
4
5
6
7
8
9
@Configuration
@ConditionalOnClass({TimeFunction.class}) // 如果有这个类字节码才加载
@EnableConfigurationProperties({TimeProperties.class}) // IOC 属性类
public class DateFormatAutoConfiguration {
@Bean
public TimeFunction timeFunction(){
return new TimeFunction();
}
}

Properties

1
2
3
4
5
6
7
8
9
10
11
12
@ConfigurationProperties(prefix = "hang.time")
public class TimeProperties {

private String format = "yyyy-MM-dd HH:mm:ss";

public String getFormat() {
return format;
}
public void setFormat(String format) {
this.format = format;
}
}

yml配置 (日期格式化自定义)

1
2
3
hang:
time:
format: yyyy年MM月dd日 HH:mm:ss

自定义函数

1
2
3
4
5
6
7
8
9
10
11
// 要托管这个类 -> @Bean注解将这个类在@Component
public class TimeFunction {
@Autowired
private TimeProperties timeProperties;
public String showTime(String name){
Date date = new Date();
DateFormat df = new SimpleDateFormat(timeProperties.getFormat());
String format = df.format(date);
return "欢迎您:" + name + "现在是:" + format;
}
}
  1. 定义一个starter模块

这个模块主要是引入autoconfiguration模块

  1. 定义spring.factories文件(位于autoconfiguration的resource/META-INF下)
1
2
3
# 使用SpringBoot自动配置类文件 读取配置类AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hang.dateFormatAutoConfiguration.DateFormatAutoConfiguration

如果不用starter自动配置,手动配置,需要配置属性文件解析Configuration-processor依赖

配置文件类通过@ConfigurationProperties(profix=””)读取配置文件

通过@Component注入spring,方法等用@Bean注入

@ComponentScan

排除过滤器
AutoConfigurationExcludeFilter

对于里面的过滤器分析:(案例)

如果排除中有一个符合条件, 则排除扫描排除

1
2
3
4
5
6
7
8
9
@ComponentScan(excludeFilters = {
@ComponentScan.Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
),@ComponentScan.Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)
})

例如我有一个配置类

1
2
3
4
5
6
7
@Configuration
public class AppConfig(){
@Bean
public OrderService orderService(){
return new OrderService();
}
}

假如我在 resource下的META_INF下面的spring.factories中

1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hang.AppConfig

众所周知, 在这个文件下配置了扫描路径也会被解析, 那么则会被解析两次.

如果配置了此项, 则只会被解析一次

TypeExcludeFilter

配置此项需要单独定义一个排除过滤器, 继承自 TypeExcludeFilter, 重写match方法

例如我需要排除我的UserService

则通过重写类形参中的MetadataReader获取类信息然后判断即可

1
2
3
4
5
6
7
public class HangTypeExcludeFilter extends TypeExcludeFilter{
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory){
return metadataReader.getClassMetadata().getClassName()
.equals(UserService.class.getName());
}
}

光配置了此项还不够, 因为在执行当前方法过滤时, 并不知道容器中有没有扫描到或者注册bean(大概), 从而导致已经开始过滤, 然后导致失效

我们在扫描之间就得放置一些bean, 该怎么做呢?

在spring.factories中

1
2
3
4
5
6
7
8
# 初始化器在创建容器对象后,扫描前执行
+ # Initializersa
+ org.springframework.context.ApplicationContextInitializer=\
+ com.hang.ApplicationContextInitializer

# Auto Configure # -> 刚刚在上一个案例写的
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hang.AppConfig

自定义一个初始化器

HangApplicationContextInitializer

1
2
3
4
5
6
7
8
public class HangApplicationContextInitializer implements ApplicationContextInitializer{
@Override
public void initialize(ConfigurableApplicationContext applicationContext){
// 添加一个单例bean(排除过滤器)
applicationContext.getBeanFactory()
.regiseterSingleton("HangTypeExcludeFilter",new HangTypeExcludeFilter());
}
}

此时则刚刚写的HangTypeExcludeFilter 中的 match匹配过滤则会生效了

SpringBoot Session整合Redis

1
2
3
4
5
6
7
8
9
<!--springboot整合redis session-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

修改Session未同步问题

当前问题仅仅适用于当前案例情况

在Session未整合Redis之前, 对数据的操作为引用数据类型, 即修改已存在的session值, 自动同步

案例代码
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
/**
* 添加购物车
*/
@PostMapping("/addCart")
public Result addCart(@RequestParam("fid") Integer fid,
@RequestParam("num") Integer num,
HttpSession session) {
// 根据fid去查商品
Resfood food = resfoodService.getById(fid);
if (Objects.isNull(food)) {
return Result.fail("没有此商品");
}
// 从session去Cart(map)
Map<Integer, CartItem> cart = null;
if (Objects.nonNull(session.getAttribute("cart"))) {
cart = (Map<Integer, CartItem>) session.getAttribute("cart");
} else {
session.setAttribute("cart", cart);
}
CartItem cartItem;
// 判断这个视频在map是否有
if (cart.containsKey(fid)) {
cartItem = cart.get(fid);
cartItem.setNum(cartItem.getNum() + 1);
cart.put(fid, cartItem);
} else {
cartItem = new CartItem();
cartItem.setNum(num);
cartItem.setResfood(food);
cart.put(fid, cartItem);
}
// 处理数量
if (cartItem.getNum() <= 0) {
cart.remove(fid);
}
// 问题出现处: 原代码没有加这个
+ session.setAttribute("cart", cart);
// 成功
return Result.*ok*(cart.values());
}

样例中第37行处, 在没有把Session添加到Redis中前, 用户在对session的值修改后, tomcat自动同步更新session的数据, 即添加购物车不会出现 数量限制问题

但是将Session添加到Spring session data redis中的时候, 从一开始的引用类型变成了类似于值引用类型了, 即引用redis里面的session值, 并不会进行同步修改

经查源码: 在Spring整合Redis Session时采用了 事件监听 的方式, 即需要再次重复设置session的操作session.setAttribute(), Redis中的Session才会同步修改

SpringBoot Mockito测试

案例 Controller层

当前测试环境:JDK8 , SpringBoot2.7.3 , Junit4, MyBatis-Plus3.x

首先第一步先创建一个简单的MVC框架的测试用例,我们在Controller中右键Go to创建测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/user")
public class UserController{
@Autowired
private UserService userService;

@GetMapping("/getById")
public User getById(@RequestParam("id") Integer id){
return userService.getUserInfoById(id);
}
@PostMapping("/save")
public String save(@RequestBody User user){
return userService.saveUserInfo(user)
? "保存成功" : "保存失败";
}
}

测试包

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
@Slf4j
@RunWith(SpringRunner.class) // Junit4
@SpringBootTest(classes = {MockApplication.class},
// 在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 自动配置和启用MockMvc配置 如果不加这个注解 下面是不能注入的
@AutoConfigureMockMvc(addFilters = false)
public class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
public void getById() {
// 预期对象
User user = new User(1, "w", "a", "悟空",
"song@foxmail.com", "13283718600", "男",
1, null, "天子脚下");
// 使用注入的mockMvc模拟发送请求
// MockMvcRequestBuilders: 请求构建器
try {
Integer id = 1;
// 在请求后面还可以指定请求类型 例如APPLICATION_JSON_VALUE
mockMvc.perform(MockMvcRequestBuilders.get("/user/getById")
.contentType(MediaType.APPLICATION_JSON_VALUE)
// 这里还能设置header
.param("id","1")
)
// MockMvcResultMatchers: mockMvc结果匹配器
.andExpect(MockMvcResultMatchers.status().isOk()) // 例如当前为 匹配状态码为200或者其他可以表示成功的
// 这个是用于取返回body的 $.name代表取body里面的name字段 假如还有
.andExpect(MockMvcResultMatchers.jsonPath("$.name", Matchers.equalToIgnoringCase(user.getName())))
// 如果状态码符合预期 则进行print输出响应信息
.andDo(MockMvcResultHandlers.print())
// 真正请求得到响应的内容,真正执行的返回值
.andReturn();
} catch (Exception e) {
log.error("当前方法为:{},发生了异常", "getById");
throw new RuntimeException(e);
}
}

@Test
public void save() {
}
}

在当前测试用例中

  1. 注意到@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT),其中它代表 :在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题

  2. @AutoConfigureMockMvc(addFilters = false) : 这个用于自动配置和启用MockMvc配置

  3. 注入mockMvc

    1
    2
    @Autowired
    private MockMvc mockMvc;
  4. 在mockMvc实例中,使用perform执行一个HTTP请求, 入口参数为RequestBuilder,输出结果为ResultAction

  5. perform中,使用MockMvcRequestBuilders请求构建器构造请求, 可以使用get或者post请求,在get或post里面写入请求路径,在后面还可以加请求内容类型contentType(MediaType.APPLICATION_JSON_VALUE),其中还能像SpringMVC一样设置其他的比如header,param

  6. andExpect期望, 使用MockMvcResultMatchers :mockMvc结果匹配器断言, 在我的测试用例中,使用了两个匹配断言

    6.1 、.andExpect(MockMvcResultMatchers.status().isOk()): 用于匹配返回状态码是否为ok,其中一般指200

    6.2、.andExpect(MockMvcResultMatchers.jsonPath("$.name", Matchers.equalToIgnoringCase(user.getName()))),其中user为自定义的期望对象, 对于jsonPath对象,一般用于取返回的Body对象, 对于当前返回的结果如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    MockHttpServletResponse:
    Status = 200
    Error message = null
    Headers = [Content-Type:"application/json"]
    Content type = application/json
    Body = {"uid":1,"username":"w","password":"a","name":"悟空","email":"song@foxmail.com","phone":"13283718600","sex":"男","state":1,"code":null,"addr":"天子脚下"}
    Forwarded URL = null
    Redirected URL = null
    Cookies = []

    假如Body中的json数据有嵌套,同样也是使用如: $.games[0].name 取game[0]对象的name属性

  7. .andDo(MockMvcResultHandlers.print())

如果符合预期结果 则进行print输出响应详细的信息:包括请求头,请求体等常见的字段,比如还有ModelAndView,Response

  1. .andReturn();

真正请求得到响应的内容,真正执行的返回值,返回值可以new一个MockMvc对象返回

  1. 其中还包括一些其他的函数,详情见 SpringBoot Test Doc 文档

如果使用这种方式发生注入错误,找不到一个Bean为MockMvc的,那么使用下面这种方案注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MockApplication.class},
// 在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// 自动配置和启用MockMvc配置 如果不加这个注解 下面是不能注入的
//@AutoConfigureMockMvc(addFilters = false)
public class UserControllerTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
@DisplayName("文本响应测试用例")
public void getById() {
// 测试代码
}
@Test
public void save() {
}
}
  1. 注意到首先注入了WebApplicationContext对象,然后有一个没有初始化的MockMvc实例
  2. @Before注解: 在每一个测试用例之前调用,每一次都会使用 MockMvcBuilders对webApplicationContext进行构建,从而得到一个实例对象mockMvc
  3. @DisplayName("文本响应测试用例")注解: 作用如图例

注入方式一: 自动配置在org.springframework.boot:spring-boot-test-autoconfiguration下的web.servlet.MockMvcAutoConfiguration配置类,

1
2
3
4
5
6
7
8
// 使用以下示例进行注入
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

注入方式二: 自动配置在org.springframework.boot:spring-boot-test-autoconfiguration下的web.servlet.AutoConfigurationMockMvc注解类

1
2
3
4
5
// 1. 在当前测试类上加注解 
@AutoConfigureMockMvc(addFilters = false)
// 2. AutoWired注入MockMvc
@Autowired
private MockMvc mockMvc;

对于Get请求 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 在请求后面还可以指定请求类型 例如APPLICATION_JSON_VALUE
mockMvc.perform(MockMvcRequestBuilders.get("/user/getById")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.param("id", "1")
)
// MockMvcResultMatchers: mockMvc结果匹配器
.andExpect(MockMvcResultMatchers.status().isOk()) // 例如当前为 匹配状态码为200或者其他可以表示成功的
// 这个是用于取返回body的
.andExpect(MockMvcResultMatchers.jsonPath("$.name", Matchers.equalToIgnoringCase(user.getName())))
// 如果状态码符合预期 则进行print输出响应信息
.andDo(print())
// 真正请求得到响应的内容,真正执行的返回值
.andReturn();

对于Post请求 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// JSON 格式 -> content
mockMvc.perform(MockMvcRequestBuilders.post("/user/save")
.contentType(MediaType.APPLICATION_JSON)
.content("{\n" +
" \"uid\": \"1\",\n" +
" \"username\": \"w\"" +
"}")
// MockMvcResultMatchers: mockMvc结果匹配器
.accept(MediaType.APPLICATION_JSON))
.andDo(print());
// From 格式 -> param
mockMvc.perform(MockMvcRequestBuilders.post("/user/save")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("uid", "1")
.param("username", "w")
.accept(MediaType.APPLICATION_JSON))
.andDo(print());

案例 Service层

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
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MockApplication.class},
// 在每一次进行mock测试时 启用随机端口启动 为例避免多个测试用例同时启动时不会产生端口占用问题
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserServiceTest {

@MockBean // 模拟Bean:Mockito 会帮我们创建一个假的 Mock 对象,替换掉 Spring 中已存在的那个真实的 userDao Bean
private UserMapper userMapper;

@Autowired
private UserService userService;

@Test
@DisplayName("测试没有dao数据访问层")
public void testEmptyMapper() {
User expectUser = new User(1, "测试没有dao数据访问层", "11", "测试没有dao数据访问层","song@foxmail.com", "13283718600", "男", 1, null, "天子脚下");
// Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 )
Mockito.when(userMapper.selectById(1))
.thenReturn(new User(1, "测试没有dao数据访问层", "11", "测试没有dao数据访问层","song@foxmail.com", "13283718600", "男", 1, null, "天子脚下"));

User user = userService.getUserInfoById(1); // 访问模拟的Dao
Assert.assertEquals(expectUser,user); // 断言正确
}

@Test
@DisplayName("测试Service")
public void testService() {
// Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 )
Mockito.when(userService.getUserInfoById(1))
.thenReturn(new User(1, "测试Service", "11", "测试没有dao数据访问层","song@foxmail.com", "13283718600", "男", 1, null, "天子脚下"));
User user = userService.getUserInfoById(1);
log.info(user.toString());
}
}

1.@MockBean: 模拟Bean:Mockito 会帮我们创建一个假的 Mock 对象,替换掉 Spring 中已存在的那个真实的 userDao Bean

2.when: 模拟引导访问的 对象.方法名()

3.thenReturn: 引导访问的返回结果

当前样例1: Mockito.anyInt(),任意的Int类型值

1
2
3
4
Mockito.when(userService.getUserById(Mockito.anyInt()))
.thenReturn(new User(3, "Calyee"));
User user1 = userService.getUserById(3); // 回传的user的名字为Calyee
User user2 = userService.getUserById(200); // 回传的user的名字也为Calyee

当前样例2: 指定的值

1
2
3
Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "Calyee"));
User user1 = userService.getUserById(3); // 回传的user的名字为Calyee -> 为预设期盼
User user2 = userService.getUserById(200); // 回传的user为null

当前样例3: 当调用Insert的时候不论传入的User字节码对象是谁,都返回100

1
2
Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100);
Integer i = userService.insertUser(new User()); //会返回100

其他注解:

4.thenThrow: 当请求方法传入的值为1时,抛出一个RuntimeException (适用于 有出参的方法)

1
2
3
Mockito.when(userService.getUserById(1))
.thenThrow(new RuntimeException("mock throw exception"));
User user = userService.getUserById(1); //会抛出一个RuntimeException

5.doThrow 如果方法没有返回值的话(即是方法定义为 public void myMethod() {…}),要改用 doThrow() 抛出 Exception, doThrow ( 适用于 没有出参的方法)

1
2
3
Mockito.doThrow(new RuntimeException("mock throw exception"))
.when(userService).print();
userService.print(); //会抛出一个RuntimeException

简单模拟SpringBoot

初始化Spring项目

1.创建maven工程,建两个Module

  1. springboot模块,表示springboot框架的源码实现
  2. user包,表示用户业务系统,用来写业务代码来测试我们所模拟出来的SpringBoot

2.其中SpringBoot也是依赖于Spring,我们需要处理请求,故需要导入SpringMVC,当然还有Tomcat等依赖

导入依赖 (其中Java JDK版本为1.8)

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
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.11.RELEASE</version>
</dependency>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.65</version>
</dependency>
</dependencies>

在User测试模块中引入刚刚创建的SpringBoot模块(这个是另外一个模块的)

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springboot</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

在User模块中创建一个标准的测试结构

Controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class UserController {

@Autowired
private UserService userService;

@GetMapping("/test")
public String test(){
return userService.test();
}
}

Service

1
2
3
4
5
6
@Service
public class UserService {
public String test(){
return "Hello My SpringBoot!";
}
}

核心注解与核心类

启动类

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @ClassName MyApplication
* @Description 自定义SpringBoot
* @Author QiuLiHang
* @Version 1.0
*/
@HangSpringBootApplication // 该注解里面包含了ComponentScan注解
public class MyApplication {
public static void main(String[] args) {
HangSpringApplication.run(MyApplication.class);
}
}

在springboot模块中创建

启动类标记注解

1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan // 默认扫描当前解析类的包路径,传给run方法的类
public @interface HangSpringBootApplication {
}

启动SpringBoot应用的启动类 -> run()方法

1
2
3
4
public class HangSpringApplication {
public static void run(Class clazz){
}
}

run方法

首先我们需要的是,启动类执行完成,就可以像原生SpringBoot一样在浏览器上可以访问到localhost:8081/test 方法

那么此时肯定要启动Tomcat容器,要想处理请求,在Spring容器中添加创建的DispatcherServlet对象添加到Tomcat里面,最后启动Tomcat

创建Spring容器

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HangSpringApplication {
public static void run(Class clazz){
// 创建Spring容器
AnnotationConfigWebApplicationContext applicationContext =
new AnnotationConfigWebApplicationContext();
// 注册: clazz -> Spring容器的配置类
applicationContext.register(clazz);
// 刷新容器,加载被扫描的bean
applicationContext.refresh();
// 启动Tomcat (此处未实现启动其他服务器)
startTomcat(applicationContext);
}
}

通过AnnotationConfigWebApplicationContext创建的容器,把传入的clazz作为容器的配置类, 比如此时我传入的是MyApplication.class类,当前类作为配置类,由于当前类头上带有我们刚刚自定义的启动类注解@HangSpringBootApplication, 其中当前注解内部带有扫描注解 @ComponentScan 那么将会自动默认扫描当前解析类的包路径(传入run方法的类包路径),假如设置了明示了扫描路径,则扫描自定义路径, 那么则会扫描到我们的UserService 和 UserController , 然后添加到Spring容器里面

一般情况 我们会直接把启动类当成配置类

而不会进行如下操作: 在其他类中创建启动类,然后在main方法(不是启动类了,没有带启动类标识注解)中进行HangSpringApplication.run(MyApplication.class)操作 (启动类标识@HangSpringBootApplication当前标明在MyApplication类上)

启动Tomcat

对于SpringBoot,它使用的是内嵌Tomcat方式,对于内嵌Tomcat,我们需要进行配置

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
public static void startTomcat(WebApplicationContext applicationContext){
// 启动 Tomcat
Tomcat tomcat = new Tomcat();

Server server = tomcat.getServer();
Service service = server.findService("Tomcat");

Connector connector = new Connector();
connector.setPort(8081); // 连接端口

Engine engine = new StandardEngine();
engine.setDefaultHost("localhost");// 本地测试

Host host = new StandardHost();
host.setName("localhost");

String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());

host.addChild(context);
engine.addChild(host);

service.setContainer(engine);
service.addConnector(connector);

// SpringMVC 处理请求 注册DispatcherServlet
tomcat.addServlet(contextPath, "dispatcher",
new DispatcherServlet(applicationContext));// 传入Spring容器
context.addServletMappingDecoded("/*", "dispatcher");

try {
tomcat.start();
} catch (LifecycleException e) {
e.printStackTrace();
}

}

此时就能启动Tomcat了,可以正常启动了,其中控制台会输出tomcat启动情况,例如刚刚设置的8081端口

浏览器访问localhost:8081/test,结果返回了"Hello My SpringBoot!"

修改其他的服务器

1.在springboot中创建一个抽象方法为:WebServer

1
2
3
4
5
6
7
8
9
/**
* 对自定义服务器调用 实现抽象化
*/
public interface WebServer {
/**
* 服务启动抽象接口
*/
public void start(WebApplicationContext applicationContext);
}

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @ClassName TomcatWebServer
* @Description Tomcat服务
* @Author QiuLiHang
* @Version 1.0
*/
public class TomcatWebServer implements WebServer{
/**
* Tomcat服务启动类
*/

@Override
public void start(WebApplicationContext applicationContext) {
Tomcat tomcat = new Tomcat();

Server server = tomcat.getServer();
Service service = server.findService("Tomcat");

Connector connector = new Connector();
connector.setPort(8081);

Engine engine = new StandardEngine();
engine.setDefaultHost("localhost");

Host host = new StandardHost();
host.setName("localhost");

String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());

host.addChild(context);
engine.addChild(host);

service.setContainer(engine);
service.addConnector(connector);

// SpringMVC 处理请求
tomcat.addServlet(contextPath, "dispatcher",
new DispatcherServlet(applicationContext));// 传入Spring容器
context.addServletMappingDecoded("/*", "dispatcher");

try {
tomcat.start();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @ClassName JettyWebServer
* @Description Jetty服务启动类
* @Author QiuLiHang
* @Version 1.0
*/

public class JettyWebServer implements WebServer{
@Override
public void start(WebApplicationContext applicationContext) {
System.out.println("启动jetty");
}
}

以上添加了两个示例服务实现类

再次修改run方法

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
public class HangSpringApplication {
public static void run(Class clazz){
// 创建Spring容器
AnnotationConfigWebApplicationContext applicationContext =
new AnnotationConfigWebApplicationContext();
// 注册: clazz -> Spring容器的配置类
applicationContext.register(clazz);
// 刷新容器,加载被扫描的bean
applicationContext.refresh();
// 启动Tomcat -> 当前方法只能启动tomcat
// startTomcat(applicationContext); // 原方法
// 那么如何优雅的启动自定义的服务器呢? 即pom中导入的服务器
// 1. 获取WebServer类 (多态)
WebServer webServer = getWebServer(applicationContext);
// 2. 调用子类启动服务
webServer.start(applicationContext);

}
/**
* 获取Spring容器中的WebServer对象
*/
private static WebServer getWebServer(WebApplicationContext webApplicationContext) {
// key为beanName,value为Bean对象
Map<String,WebServer> webServers =
webApplicationContext.getBeansOfType(WebServer.class);// 获取WebServer类型的对象
// 限制Spring容器中的WebServer对象
// 1.不能为空
if(webServers.isEmpty()){
throw new NullPointerException();
}
// 2.只能存在一个
if(webServers.size() > 1){
throw new IllegalStateException();
}
// 返回唯一的一个
return webServers.values().stream().findFirst().get();
}
}

此时,在启动类中添加一个Tomcat 的Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@HangSpringBootApplication // 该注解里面包含了ComponentScan注解
public class MyApplication {

// Spring配置类中配置的bean
// 会被加入到SpringIOC容器里面,在后面启动WebServer的时候 会被扫描到
// 然后通过多态 启动子类实现的服务接口
@Bean
public TomcatWebServer tomcatWebServer(){
return new TomcatWebServer();
}

public static void main(String[] args) {
HangSpringApplication.run(MyApplication.class);
}
}

当然,自己在启动类中配置服务器的bean,显然耦合度太高了,至此我们需要引入新的方案

自动配置WebServer服务

首先在springboot模块中添加WebServerAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @ClassName WebServerAutoConfiguration
* @Description WebServer自动配置类
* @Author QiuLiHang
* @Version 1.0
*/
@Configuration // 交给Spring管理
public class WebServerAutoConfiguration {
// Spring配置文件
// WebServer自动配置类
@Bean
@Conditional(TomcatCondition.class)
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}

@Bean
@Conditional(JettyCondition.class)
public JettyWebServer jettyWebServer() {
return new JettyWebServer();
}
}

其中发现有一个注解为 @Conditional(TomcatCondition.class)

该注解,当该Condition.class判断返回的boolean结果,作为是否执行该方法的条件,如果返回true则该注解修饰的该方法可执行

其中TomcatCondition类需要自定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TomcatCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
// 判断项目中是否有符合条件的依赖
try {
// 利用类加载器,加载需要判断的类
conditionContext.getClassLoader().loadClass("org.apache.catalina.startup.Tomcat");
return true;// 能走到这个地方 证明有
} catch (ClassNotFoundException e) {
return false; // 没有该类
}
// // 如果返回true,则符合条件
// return false;
}
}

走到这了, 上面的目标就是,检查当前依赖是否包含修饰的依赖项, 实现自动判断

现在还会出现问题, 就是 不能像SpringBoot一样,默认Tomcat, 排除其他的服务器依赖给子项目, 此时要在父项目中的pom.xml中设置<optional>true</optional>属性: 该依赖在项目之间依赖不传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.65</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.14.v20181114</version>
<!--
加了这个设置,禁止依赖传递,即传给子类
(因为SpringBoot也是默认传递Tomcat而不传递jetty等其他)
-->
<optional>true</optional>
</dependency>

此时, 如果在子模块中想切换jetty ,在依赖父项目处使用exclusion排除父项目传递的依赖, 再添加其他依赖, 示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<dependency>
<groupId>com.hang</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<!--排除tomcat-->
<exclusions>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--切换为jetty-->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.14.v20181114</version>
</dependency>
</dependencies>

模拟条件注解

在上面的模拟方案, 基于两个服务器的选择从而定义了两个Condition类: 1.TomcatCondition 2.JettyCondition

大胆想象, 加入有很多服务器需要筛选, 然后发现全部都是重复的代码, 何不如封装一下呢

仿SpringBoot的ConditionalOnClass编写一个HangConditionalOnClass.

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(HangCondition.class) // 封装的条件排除类
public @interface HangConditionalOnClass {
/**
* 拿到的类名
* @return
*/
String value();
}

HangCondition类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HangCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
// AnnotatedTypeMetadata 里面有解析到HangConditionalOnClass注解的Value信息
// 因为是解析到HangConditionalOnClass注解才进来这里的
Map<String, Object> annotationAttributes = annotatedTypeMetadata.getAnnotationAttributes(HangConditionalOnClass.class.getName());
String values = (String) annotationAttributes.get("value"); // 拿到Value
// 判断项目中是否有符合条件的依赖
try {
// 利用类加载器,加载需要判断的类
conditionContext.getClassLoader().loadClass(values);
return true;// 能走到这个地方 证明有
} catch (ClassNotFoundException e) {
return false; // 没有该类
}
}
}

自动配置类

有了条件注解,我们就可以来使⽤它了,那如何实现呢?这⾥就要⽤到⾃动配置类的概念,我们先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebServiceAutoConfiguration{
@Bean
@HangConditionalOnClass("org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer(){
return new TomcatWebServer();
}
@Bean
@HangConditionalOnClass("org.eclipse.jetty.server.Server")
public JettyWebServer jettyWebServer(){
return new JettyWebServer();
}
}

这个代码跟最开始的大同小异, 只不过加了我们的条件注解

要实现服务器自动配置只需要在过滤掉所有bean留下唯一的bean就行了

这样整体SpringBoot启动逻辑就是这样的:

  1. 创建⼀个AnnotationConfigWebApplicationContext容器
  2. 解析MyApplication类,然后进⾏扫描
  3. 通过getWebServer⽅法从Spring容器中获取WebServer类型的Bean
  4. 调⽤WebServer对象的start⽅法

有了以上步骤,我们还差了⼀个关键步骤,就是Spring要能解析到WebServiceAutoConfiguration这个⾃动配置类,因为不管这个类⾥写了什么代码,Spring不去解析它,那都是没⽤的,此时我们需要SpringBoot在run⽅法中,能找到WebServiceAutoConfiguration这个配置类并添加到Spring容器中。

基于包扫描, 在SpringBoot中自己实现了一套SPI机制, 也就是熟悉的spring.factories

发现自动配置类

基于SPI机制, SpringBoot约定在项目的resource目录下的META_INF中创建spring.factories, 配置SpringBoot中所需扫描的类

并且提供一个接口

HangAutoConfiguration

1
2
public interface HangAutoConfiguration{
}

然后在WebServiceAutoConfiguration实现此接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebServiceAutoConfiguration implements HangAutoConfiguration{
@Bean
@HangConditionalOnClass("org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer(){
return new TomcatWebServer();
}
@Bean
@HangConditionalOnClass("org.eclipse.jetty.server.Server")
public JettyWebServer jettyWebServer(){
return new JettyWebServer();
}
}

然后利用spring中的@Import技术导入这些配置类, 我们在@HangSpringBootApplication的定义上增加

1
@Import(HangImportSelect.class)
1
2
3
4
5
6
7
8
9
10
11
public class ZhouyuImportSelect implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
ServiceLoader<AutoConfiguration> serviceLoader = ServiceLoader.load(AutoConfiguration.class);
List<String> list = new ArrayList<>();
for(AutoConfiguration autoConfiguration : serviceLoader) {
list.add(autoConfiguration.getClass().getName());
}
return list.toArray(new String[0]);
}
}

这就完成了从com.zhouyu.springboot.AutoConfiguration⽂件中获取⾃动配置类的名字,并导⼊到Spring容器中,从⽽Spring容器就知道了这些配置类的存在,⽽对于user项⽬⽽⾔,是不需要修改代码的。