SpringBoot 2 开发随笔
Spring Boot 配置
邮件发送失败问题
在本地开发环境测试,Spring Boot 能够正常发送邮件,但部署到阿里云 ECS 服务器以后,一直没有收到邮件,部分关键日志信息如下:
1 | 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; |
从现有情况看,跟程序运行环境有关,查看相关资料,发现在阿里云 ECS 服务器上,默认禁用了 25 端口,所以在通过 25 端口去连接邮件服务器时,无法连上,就报超时了。官方建议使用 465 端口,而 456 端口是 SSL 协议的,所以不仅要换端口,还需要进行 SSL 协议替换。下面是在 application.properties
进行的邮件发送相关配置,经过这样配置后,在阿里 ECS 上就能够正常发送邮件了
1 | # Mail Config |
163 邮箱相关服务器信息如下:
激活不同的配置文件
使用命令行运行 SpringBoot 应用时,可以通过 --spring.profiles.active
来激活不同环境的配置文件。
1 | java -jar order-service-v1.0.jar --spring.profiles.active=dev |
日期的 JSON 序列化格式
SpringBoot 设置日期的 JSON 序列化格式,有以下两种方式:
方式一,在
application.yml
配置文件中,通过设置 Jackson 的配置来指定 JSON 序列化时的日期格式
1 | spring: |
方式二,在相应的类的属性上使用 @JsonFormat 注解
1 | public class Example { |
@JsonFormat
注解用于控制 Java 对象在序列化(转为 JSON 字符串)和反序列化(从 JSON 字符串转为 Java 对象)时的格式。它通常与 Jackson 序列化库一起使用,作用于 JSON 数据的输入和输出。@DateTimeFormat
注解主要用于 Spring MVC 中的数据绑定。它控制的是将表单数据(通常是字符串)绑定到 Java 对象(POJO)的日期属性时的格式。它作用于 Spring 的表单提交和处理流程中。- 如果在业务开发中,有一个日期属性需要在 Web 表单提交时格式化输入,同时又需要在返回 JSON 响应时格式化输出,那么就可以同时使用
@DateTimeFormat
和@JsonFormat
注解。
bootstrap.yml 与 application.yml 的区别
加载顺序
bootstrap.yml
>application.yml
>application-dev.yml
。bootstrap.yml
作用于应用程序上下文的引导阶段,bootstrap.yml
由父 Spring ApplicationContext 加载。- 如果
bootstrap.yml
和application.yml
在同一目录下时,则bootstrap.yml
先加载,application.yml
后加载。 - 如果
application.properties
和application.yml
在同一目录下时,且存在相同的配置,则application.properties
会覆盖application.yml
里面的属性,因为application.properties
会后加载,也就是说哪个文件被最后加载,哪个才具有最高级。
配置区别
bootstrap.yml
和application.yml
都可以用来配置参数。bootstrap.yml
用来程序引导时执行,应用于更加早期配置信息读取,可以理解成系统级别的一些参数配置,这些参数一般是不会随意变动的。application.yml
用来定义应用级别的配置参数,即应用程序特有的配置信息,可以用来配置后续各个模块中需使用的公共参数等。如果加载的application.yml
的内容标签与bootstrap.yml
的标签一致,那么application.yml
会覆盖bootstrap.yml
, 而application.yml
里面的内容可以动态替换。
的典型应用场景(bootstrap.yml)
- 一些配置信息加密 / 解密。
- 一些固定的不能被覆盖的配置信息。
- 比如,使用 Spring Cloud Config Server 的时候,应该在
bootstrap.yml
里面指定spring.application.name
和spring.cloud.config.server.git.uri
。这是因为当使用 Spring Cloud 的时候,配置信息一般是从 Config Server 加载的,为了取得配置信息(比如密码等),需要一些提早的或引导配置。因此,将 Spring Cloud Config Server 的配置放在bootstrap.yml
,用来加载真正需要的配置信息。
扫描父模块的 Mapper 接口与 XML 映射文件
假设有 common
和 shop
两个模块,common
模块里有 MyBatis 的 Entity 类、Mapper 接口和 XML 映射文件,而 shop
模块则依赖了 common
模块,此时若在 shop
模块中无法注入 common
模块的 Mapper,则可以参考以下方法解决问题。
- 第一步,先让
shop
模块可以正常扫描到common
模块的 XML 映射文件和 Entity 类,shop
模块的 YAML 配置内容如下:
1 | mybatis: |
提示
值得一提的是,在 shop
模块中的 application.yml
里面,配置 MyBatis 的 mapper-locations
时,若使用的是 classpath
,那么只会扫描当前模块的 XML 映射文件,而使用 classpath*
则会扫描所有 Jar 包下的 XML 映射文件。
- 第二步,在
shop
模块的启动类上添加@MapperScan
注解,这是为了可以扫描到common
模块的 Mapper 接口,而且被扫描到的 Mapper 接口,在编译之后都会自动生成相应的实现类。若common
模块没有在shop
模块的启动类可以扫描的包或者子包下面,那么还需要在shop
模块的启动类上添加@ComponentScan
注解,这样才能让 SpringBoot 扫描到其他模块中的 Bean 类,示例代码如下:
1 | package com.shop; |
提示
@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 | <parent> |
注解使用
添加 @RunWith
、@SpringBootTest
注解
1 |
|
其中有两个 runner
可以选择,分别是 SpringRunner
和 SpringJUnit4ClassRunner
。如果是在 Junit 4.3 之前,只能选择 SpringJUnit4ClassRunner
,如果是 Junit 4.3 之后,建议选择 SpringRunner
,其中 SpringRunner
仅仅继承了 SpringJUnit4ClassRunner
,没有任何的额外代码。
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 |
数据库测试
在测试数据操作的时候,若不想让测试数据污染数据库,只需要给测试类或者测试方法添加 @Transactional
注解即可,这样既可以测试数据操作方法,又不会污染数据库,即默认会回滚对数据库的所有写操作。
1 |
|
Web 模拟测试
在 Spring Boot 项目里面可以直接使用 Junit 对 Web 项目进行测试,Spring 提供了 TestRestTemplate
对象,使用这个对象可以很方便的进行请求模拟。Web 测试只需要进行两步操作:
- 在
@SpringBootTest
注解上设置webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
,即使用随机端口 - 使用
TestRestTemplate
类进行 POST 或 GET 请求
1 |
|
其中 getForObject()
的含义代表执行 GET 请求,并返回 Object
类型的结果,第二个参数表示将返回结果转换为 String
类型,更多的请求方法如下:
- getForEntity:Get 请求,返回实体对象(可以是集合)
- postForEntity:Post 请求,返回实体对象(可以是集合)
- postForObject:Post 请求,返回对象
SpringBoot 应用启动
通过命令指定端口启动应用
使用 java -jar
命令启动 SpringBoot 应用时,可以通过添加命令行参数来指定服务端口。具体方法如下:
方法一:通过
--server.port
参数指定
可以在启动命令中使用 --server.port
参数来指定端口号,例如:
1 | java -jar your-application.jar --server.port=8081 |
方法二:通过
--spring.application.json
参数指定
可以通过设置 --spring.application.json
参数来指定端口号,这种方式可以在启动时设置多个配置项,例如:
1 | java -jar your-application.jar --spring.application.json='{"server":{"port":8081}}' |
方法三:通过
-D
系统属性指定
可以通过 Java 的系统属性来指定端口号,例如:
1 | java -jar -Dserver.port=8081 your-application.jar |
方法四:通过外部配置文件指定
如果 SpringBoot 项目有一个外部的配置文件(如 application.properties
或 application.yml
),则可以通过指定该文件来覆盖默认配置:
1 | java -jar your-application.jar --spring.config.location=/path/to/your/application.properties |
然后在这个外部配置文件中,可以设置端口:
1 | 8081 = |
最近实践
推荐的方法是使用 --server.port
参数指定端口,因为它最为简洁明了。如果有其他复杂配置需求,可以考虑使用 --spring.application.json
参数或者外部配置文件。
SpringBoot 应用监控
Admin 无法监控应用的健康状态
Eureka/Nacos + Admin 结合使用时,当微服务应用在 application.yml
里配置了 server.servlet.context-path
属性之后,Admin 监控中心无法监控到微服务应用的健康状态。
1 | server: |
解决方法是微服务应用注册进 Eureka/Nacos 的时候,指定 Management 的 context-path
属性。使用 Nacos 作为注册中心,配置示例如下:
1 | server: |
使用 Eureka 作为注册中心,配置示例如下:
1 | server: |
SpringBoot 连接 MySQL
MySQL 5 连接
- 数据库连接参数
1 | com.mysql.jdbc.Driver = |
- Maven 依赖
1 | <dependency> |
MySQL 8 连接
- 数据库连接参数
1 | com.mysql.cj.jdbc.Driver = |
- Maven 依赖
1 | <dependency> |
Spring Boot 的 Maven 使用
relativePath 标签
spring-boot-starter-parent
是一个特殊的 starter
,用来提供 Maven 的默认依赖,使用它之后常用的包可以省去 version
标签,同时也可以解决版本依赖和兼容问题。比如说有些依赖包之间有版本对应,如果版本不对就会出现报错,如果没有这个特殊的 starter
,就需要去查找对应的兼容版本,有了这个 starter
,对应的版本已经配置完成,这样就不再需要关注依赖的版本兼容问题,开箱即可使用。
1 | <parent> |
<relativePath/>
标签用于指定父模块pom.xml
文件的查找路径,默认顺序为:relativePath
> 本地仓库 > 远程仓库- 不配置
<relativePath/>
标签时,默认的查找路径是../pom.xml
,会从本地路径中查找父模块的pom.xml
- 配置
<relativePath/>
后,会从本地仓库查找,本地仓库查找不到就从远程仓库查找 - 配置
<relativePath>xxx/pom.xml</relativePath>
,会从本地指定的路径查找
两种依赖引入方式
第一种引入方式
使用 <parent>
标签引入,这种方式无法解决 Maven 的单继承问题。
1 | <parent> |
第二种引入方式
使用 <dependencyManagement>
标签引入,使用这种方式就不用继承父模块,可以解决单继承的问题。这样就可以继承其他父模块,比如自己创建的父模块。
1 | <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 | <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 | Manifest-Version: 1.0 |
在不可执行 Jar 包中,也存在 META-INF/MANIFEST.MF
文件,但是文件中没有定义启动类等配置信息,其中的文件内容如下:
1 | Manifest-Version: 1.0 |
值得一提的是,不可以执行 Jar 包不会将项目的依赖一起打包进去。两个 Jar 包的内部结构是完全不同的,因此一个可以直接执行,另一个则可以被其他项目依赖。
同时打包成两个 Jar 包
一般来说,Spring Boot 项目直接打包成可执行 Jar 就可以了,不建议将 Spring Boot 项目作为普通的 Jar 被其他的项目所依赖。如果有这种需求,建议将被依赖的部分,单独抽出来做一个普通的 Maven 模块,然后在其他项目中引用这个 Maven 模块。如果希望 Spring Boot 的 Maven 插件同时生成可执行 Jar 包和普通可依赖的 Jar 包,可以使用以下的插件配置内容:
1 | <build> |
classifier
表示可执行 Jar 包的文件后缀名称,这样在执行 repackage
命令时,就不会给 mvn package
命令所打成的 Jar 包重命名为 *.original
文件。