SpringBoot 2.x 开发随笔

Spring Boot 配置

邮件发送

在本地开发环境测试,Spring Boot 能够正常发送邮件,但部署到阿里云 ECS 服务器以后,一直没有收到邮件,部分关键日志信息如下:

1
2
3
4
5
6
7
8
org.springframework.mail.MailSendException: Mail server connection failed; nested exception is com.sun.mail.util.MailConnectException: Couldn't connect to host, port: smtp.163.com, 25; timeout -1;
nested exception is:
java.net.ConnectException: 连接超时 (Connection timed out). Failed messages: com.sun.mail.util.MailConnectException: Couldn't connect to host, port: smtp.163.com, 25; timeout -1;
nested exception is:
java.net.ConnectException: 连接超时 (Connection timed out)
at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:447)
at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:322)
at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:311)

从现有情况看,跟程序运行环境有关,查看相关资料,发现在阿里云 ECS 服务器上,默认禁用了 25 端口,所以在通过 25 端口去连接邮件服务器时,无法连上,就报超时了。官方建议使用 465 端口,而 456 端口是 SSL 协议的,所以不仅要换端口,还需要进行 SSL 协议替换。下面是在 application.properties 进行的邮件发送相关配置,经过这样配置后,在阿里 ECS 上就能够正常发送邮件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Mail Config
spring.mail.host=smtp.163.com
spring.mail.username=xxx@163.com
spring.mail.password=xxxxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

# SSL Config
spring.mail.port=465
spring.mail.protocol=smtp
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.socketFactory.port=465
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory

163 邮箱相关服务器信息如下:

spring-boot-mail

激活不同的配置文件

使用命令行运行 SpringBoot 应用时,可以通过 --spring.profiles.active 来激活不同环境的配置文件。

1
java -jar order-service-v1.0.jar --spring.profiles.active=dev

bootstrap.yml 与 application.yml 的区别

加载顺序

  • bootstrap.yml > application.yml > application-dev.yml
  • bootstrap.yml 作用于应用程序上下文的引导阶段,bootstrap.yml 由父 Spring ApplicationContext 加载
  • bootstrap.ymlapplication.yml 在同一目录下时,bootstrap.yml 先加载,application.yml 后加载
  • application.propertiesapplication.yml 在同一目录下时,且存在相同的配置,则 application.properties 会覆盖 application.yml 里面的属性,因为 application.properties 会后加载,也就是说哪个文件被最后加载,哪个才具有最高级

配置区别

  • bootstrap.ymlapplication.yml 都可以用来配置参数
  • bootstrap.yml 用来程序引导时执行,应用于更加早期配置信息读取,可以理解成系统级别的一些参数配置,这些参数一般是不会变动的,一旦 bootStrap.yml 被加载,则内容不会被覆盖
  • application.yml 用来定义应用级别的配置参数,即应用程序特有的配置信息,可以用来配置后续各个模块中需使用的公共参数等。如果加载的 application.yml 的内容标签与 bootstrap.yml 的标签一致,那么 application.yml 会覆盖 bootstrap.yml, 而 application.yml 里面的内容可以动态替换

典型应用场景

  • 一些加密 / 解密的场景
  • 一些固定的不能被覆盖的属性
  • 当使用 Spring Cloud Config Server 的时候,应该在 bootstrap.yml 里面指定 spring.application.namespring.cloud.config.server.git.uri。这是因为当使用 Spring Cloud 的时候,配置信息一般是从 Config Server 加载的,为了取得配置信息(比如密码等),需要一些提早的或引导配置。因此,把 Config Server 信息放在 bootstrap.yml,用来加载真正需要的配置信息。

扫描父模块的 Mapper 接口与 XML 映射文件

假设有 commonshop 两个模块,common 模块里有 MyBatis 的 Entity 类、Mapper 接口和 XML 映射文件,而 shop 模块则依赖了 common 模块,此时若在 shop 模块中无法注入 common 模块的 Mapper,则可以参考以下方法解决问题。

  • 第一步,先让 shop 模块可以正常扫描到 common 模块的 XML 映射文件和 Entity 类,shop 模块的 YAML 配置内容如下:
1
2
3
mybatis:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.common.**.entity

提示

值得一提的是,在 shop 模块中的 application.yml 里面,配置 MyBatis 的 mapper-locations 时,若使用的是 classpath,那么只会扫描当前模块的 XML 映射文件,而使用 classpath* 则会扫描所有 Jar 包下的 XML 映射文件。

  • 第二步,在 shop 模块的启动类上添加 @MapperScan 注解,这是为了可以扫描到 common 模块的 Mapper 接口,而且被扫描到的 Mapper 接口,在编译之后都会自动生成相应的实现类。若 common 模块没有在 shop 模块的启动类可以扫描的包或者子包下面,那么还需要在 shop 模块的启动类上添加 @ComponentScan 注解,这样才能让 SpringBoot 扫描到其他模块中的 Bean 类,示例代码如下:
1
2
3
4
5
6
7
8
package com.shop;

@SpringBootApplication
@MapperScan("com.common.**.mapper")
@ComponentScan(basePackages = {"com.common"})
public class ShopApplication {

}

提示

  • @MapperScan("com.common.mapper"):扫描指定包中的接口
  • @MapperScan("com.common.*.mapper"):一个 * 代表任意字符串,但只代表一级包,比如可以扫到 com.common.aaa.mapper,不能扫到 com.common.aaa.bbb.mapper
  • @MapperScan("com.common.**.mapper"):两个 * 代表任意数量的包,比如可以扫到 com.common.aaa.mapper,也可以扫到 com.common.aaa.bbb.mapper

Spring Boot 单元测试

基础使用

引入 Maven 依赖

1
2
3
4
5
6
7
8
9
10
11
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.18.RELEASE</version>
</parent>

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

添加 @RunWith@SpringBootTest 注解

1
2
3
4
5
6
7
8
9
@RunWith(SpringRunner.class)
@SpringBootTest
public class SimpleTest {
@Test
public void doTest() {
int num = new Integer(1);
Assert.assertEquals(num, 1);
}
}

其中有两个 runner 可以选择,分别是 SpringRunnerSpringJUnit4ClassRunner。如果是在 Junit 4.3 之前,只能选择 SpringJUnit4ClassRunner,如果是 Junit 4.3 之后,建议选择 SpringRunner,其中 SpringRunner 仅仅继承了 SpringJUnit4ClassRunner,没有任何的额外代码。

1
2
3
4
5
6
7
8
9
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class SimpleTest {
@Test
public void doTest() {
int num = new Integer(1);
Assert.assertEquals(num, 1);
}
}

注解说明

  • @RunWith:标识为 JUnit 的运行环境
  • @SpringBootTest:获取启动类、加载配置,确定装载 Spring Boot
  • @Test:声明需要测试的方法
  • @BeforeClass:针对所有测试,只执行一次,且必须被 static void 修饰
  • @AfterClass:针对所有测试,只执行一次,且必须被 static void 修饰
  • @Before:每个测试方法运行前都会执行的方法
  • @After:每个测试方法运行后都会执行的方法
  • @Ignore:忽略方法

断言测试

断言测试也就是期望值测试,是单元测试的核心,也就是决定测试结果的表达式,Assert 对象中的断言方法如下:

  • Assert.assertEquals:对比两个值相等
  • Assert.assertNotEquals:对比两个值不相等
  • Assert.assertSame:对比两个对象的引用相等
  • Assert.assertArrayEquals:对比两个数组相等
  • Assert.assertTrue:验证返回是否为真
  • Assert.assertFlase:验证返回是否为假
  • Assert.assertNull:验证 Null
  • Assert.assertNotNull:验证非 Null

超时测试

@Test 注解设置 timeout 属性即可,时间单位为毫秒:

1
@Test(timeout = 1000)

数据库测试

在测试数据操作的时候,若不想让测试数据污染数据库,只需要给测试类或者测试方法添加 @Transactional 注解即可,这样既可以测试数据操作方法,又不会污染数据库,即默认会回滚对数据库的所有写操作。

1
2
3
4
5
6
7
8
9
10
11
@Test
@Transactional
public void saveTest() {
User user = new User();
user.setName("Adam");
user.setAge(19);
user.setPwd("123456");
userRepository.save(user);
System.out.println("userId:" + user.getId());
Assert.assertTrue(user.getId()>0);
}

Web 模拟测试

在 Spring Boot 项目里面可以直接使用 Junit 对 Web 项目进行测试,Spring 提供了 TestRestTemplate 对象,使用这个对象可以很方便的进行请求模拟。Web 测试只需要进行两步操作:

  • @SpringBootTest 注解上设置 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,即使用随机端口
  • 使用 TestRestTemplate 类进行 POST 或 GET 请求
1
2
3
4
5
6
7
8
9
10
11
12
13
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest {

@Autowired
private TestRestTemplate restTemplate;

@Test
public void getName() {
String name = restTemplate.getForObject("/name", String.class);
Assert.assertEquals("Adam", name);
}
}

其中 getForObject() 的含义代表执行 GET 请求,并返回 Object 类型的结果,第二个参数表示将返回结果转换为 String 类型,更多的请求方法如下:

  • getForEntity:Get 请求,返回实体对象(可以是集合)
  • postForEntity:Post 请求,返回实体对象(可以是集合)
  • postForObject:Post 请求,返回对象

Spring Boot 的 Maven 使用

relativePath 标签

spring-boot-starter-parent 是一个特殊的 starter,用来提供 Maven 的默认依赖,使用它之后常用的包可以省去 version 标签,同时也可以解决版本依赖和兼容问题。比如说有些依赖包之间有版本对应,如果版本不对就会出现报错,如果没有这个特殊的 starter,就需要去查找对应的兼容版本,有了这个 starter,对应的版本已经配置完成,这样就不再需要关注依赖的版本兼容问题,开箱即可使用。

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/>
</parent>
  • <relativePath/> 标签用于指定父模块 pom.xml 文件的查找路径,默认顺序为:relativePath > 本地仓库 > 远程仓库
  • 不配置 <relativePath/> 标签时,默认的查找路径是 ../pom.xml,会从本地路径中查找父模块的 pom.xml
  • 配置 <relativePath/> 后,会从本地仓库查找,本地仓库查找不到就从远程仓库查找
  • 配置 <relativePath>xxx/pom.xml</relativePath>,会从本地指定的路径查找

两种依赖引入方式

第一种引入方式

使用 <parent> 标签引入,这种方式无法解决 Maven 的单继承问题。

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
</parent>

第二种引入方式

使用 <dependencyManagement> 标签引入,使用这种方式就不用继承父模块,可以解决单继承的问题。这样就可以继承其他父模块,比如自己创建的父模块。

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencyManagement> 标签其实就相当于一个对 Jar 包版本进行管理的依赖管理器,如果在外面的 <dependency> 标签内没有找到 version 属性,Maven 就会去 <dependencyManagement> 标签内查找相应的版本信息。如果既使用了 <dependencyManagement> 标签,又在外面的 <dependency> 标签内指定了 version 属性,那边 Maven 会以外面的 <dependency> 标签内的 version 属性为准的,所以不用担心使用 <dependencyManagement> 标签后无法自行指定依赖的版本信息。

多模块互相引用打包运行找不到类

Spring Boot 模块打包成可执行的 Jar 包,同时被其他模块所依赖,在 IDEA 里项目运行一切正常,但使用 java -jar 命令启动其他模块时,会出现找不到依赖模块中的类的错误?这是由于还没有搞清楚可执行 Jar 和普通 Jar 到底有什么区别导致的。

可执行 Jar 与普通 Jar 的区别

  • 普通 Jar 包:可以被其他项目应用依赖,不可以使用命令 java -jar xxx.jar 运行
  • 可执行 Jar 包:不可以被其他项目应用依赖,可以使用命令 java -jar xxx.jar 运行

特别注意:Spring Boot 项目默认打包的是可执行 Jar 包,普通项目默认打包的是不可执行的 Jar 包,但是普通项目也可以打包成可执行 Jar 包。

Spring Boot 打包插件介绍

Spring Boot 项目默认的打包插件是 ​​spring-boot-maven-plugin,这个打包插件存在五个方面的功能,如下:​​

五个功能分别是:

  • ​​build-info​​:生成项目的构建信息文件 ​​build-info.properties​​
  • ​​repackage​​:这个是默认 goal,在 ​​mvn package​​ 执行之后,这个命令会再次打包生成可执行的 Jar 包,同时将 ​​mvn package​​ 生成的 Jar 包重命名为 ​​*.original
  • run:这个可以用来运行 Spring Boot 应用
  • start:这个在 ​​mvn integration-test​​ 阶段,进行 Spring Boot 应用生命周期的管理
  • stop:这个在 ​​mvn integration-test​​ 阶段,进行 Spring Boot 应用生命周期的管理

若要使用 Spring Boot 打包插件的 repackage 功能,可参考以下的配置内容。同样的,若要使用其他功能,也需要开发者显式配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</build>

Spring Boot 打包过程分析

Spring Boot 的 repackage 功能的作用,就是在打包的时候,多做一点额外的事情:

  • 第一步:mvn package 命令对项目进行打包,打成一个普通的 Jar 包,它可以被其他项目依赖,但是不可以被执行
  • 第二步:repackage 命令,再次打包项目,将之打成一个可执行 Jar 包,并将第一步生成的 Jar 包重命名为 *.original 文件

在项目的 target 目录下可以看到有两个文件,auth.jar 是可执行 Jar 包,auth.jar.original 是被重命名的可依赖的 Jar 包,如下图所示。

在可执行 Jar 包中,有一个 ​​META-INF​​ 的目录,该目录下有一个 ​​MANIFEST.MF​​ 文件,其中的文件内容如下:

1
2
3
4
5
6
7
8
9
10
Manifest-Version: 1.0
Created-By: Maven Archiver 3.4.0
Build-Jdk-Spec: 11
Implementation-Title: auth
Implementation-Version: 1.0-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.clay.auth.AuthApplication
Spring-Boot-Version: 2.2.6.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

在不可执行 Jar 包中,也存在 META-INF/MANIFEST.MF 文件,但是文件中没有定义启动类等配置信息,其中的文件内容如下:

1
2
3
4
5
Manifest-Version: 1.0
Created-By: Maven Archiver 3.4.0
Build-Jdk-Spec: 11
Implementation-Title: auth
Implementation-Version: 1.0-SNAPSHOT

值得一提的是,不可以执行 Jar 包不会将项目的依赖一起打包进去。两个 Jar 包的内部结构是完全不同的,因此一个可以直接执行,另一个则可以被其他项目依赖。

同时打包成两个 Jar 包

一般来说,Spring Boot 项目直接打包成可执行 Jar 就可以了,不建议将 Spring Boot 项目作为普通的 Jar 被其他的项目所依赖。如果有这种需求,建议将被依赖的部分,单独抽出来做一个普通的 Maven 模块,然后在其他项目中引用这个 Maven 模块。如果希望 Spring Boot 的 Maven 插件同时生成可执行 Jar 包和普通可依赖的 Jar 包,可以使用以下的插件配置内容:

1
2
3
4
5
6
7
8
9
10
11
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>

​​classifier​​ 表示可执行 Jar 包的文件后缀名称,这样在执行 ​​repackage​​ 命令时,就不会给 ​​mvn package​​ 命令所打成的 Jar 包重命名为 *.original 文件。

SpringBoot 应用监控

Admin 无法监控应用的健康状态

Eureka/Nacos + Admin 结合使用时,当微服务应用在 application.yml 里配置了 server.servlet.context-path 属性之后,Admin 监控中心无法监控到微服务应用的健康状态。

1
2
3
server:
servlet:
context-path: /shop

解决方法是微服务应用注册进 Eureka/Nacos 的时候,指定 Management 的 context-path 属性。使用 Nacos 作为注册中心,配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
servlet:
context-path: /shop

spring:
cloud:
nacos:
username: @nacos.username@
password: @nacos.password@
discovery:
server-addr: 192.168.1.30:8848
metadata:
management.context-path: ${server.servlet.context-path}/actuator

使用 Eureka 作为注册中心,配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
servlet:
context-path: /shop

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8001/eureka
registry-fetch-interval-seconds: 10
instance:
instance-id: ${spring.application.name}-${spring.cloud.client.ip-address}:${server.port}
prefer-ip-address: true
metadata-map:
management.context-path: ${server.servlet.context-path}/actuator