SpringBoot项目升级到3.*,并由JDK8升级到JDK21
文章目录
- 技术选型说明
- JDK21的Demo项目下载
- 升级过程出现的问题及解决
- 1、程序包javax.servlet.http不存在
- 1.1、java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
- 1.2、javax.validation包替换为jakarta.validation
- 1.3、jakarta的名字由来
- 2、mybatis-plus升级
- 3、mybatis-plus多数据源支持
- 4、redis配置调整
- 5、openfeign配置调整到spring.cloud下
- 6、升级到swagger3
- 6.1、swagger的新注解
- 6.2、java.lang.NoSuchMethodError: 'boolean org.apache.commons.lang3.math.NumberUtils.isCreatable(java.lang.String)'
- 7、No SLF4J providers were found.
- 其它问题
- import java.util.concurrent.TimeUnit 报错:
- FeignClient 加 GetMapping,实际自动转POST发出
- 单元测试
- 步骤
- 错误处理
- java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test
- 提示:Java.lang.Exception: No runnable methods
- 测试类不支持注解:RequiredArgsConstructor
- Caused by: org.springframework.cloud.commons.ConfigDataMissingEnvironmentPostProcessor$ImportException: No spring.config.import set
- 启动测试时,Autowired注解的类为null
技术选型说明
前几个月搞新项目,做技术选型时,评估了一下,决定使用JDK21,主要的评估点:
- JDK21已经出了LTS长期支持版本,而且按Oracle官方说明,是免费使用的:https://www.oracle.com/hk/java/technologies/downloads/#java21
JDK 21 binaries are free to use in production and free to redistribute, at no cost, under the Oracle No-Fee Terms and Conditions (NFTC).
- 从JDK8到JDK21,引入了很多的性能优化,包括GC改进,之前看到过一个性能评测,同样的代码,在JDK21也比JDK8下运行要快10%~30%,不过现在找不到那个链接了,不过google搜索一下还是有很多类似的性能评测文章的;
- SpringBoot的3.*最新版本,已经不支持JDK8了,例如现在的Stable稳定版3.3.5,要求JDK17:https://docs.spring.io/spring-boot/system-requirements.html
而SpringBoot2.*的商业支持只到2025年2月:https://spring.io/blog/2022/05/24/preparing-for-spring-boot-3-0 - 拥有经常被别人安利的虚拟线程(我还没用过)
- 新项目,没有任何历史债务,又是探索型项目,工期要求不那么急,那就让团队进步一下,搞吧。
最终决定选型:JDK21 + SpringBoot3.3.1
注:JDK21,有很多公司都推出了发行版,基本上都可以下载和使用,这里列举几个:
- oralce推出的NFTC版本:https://www.oracle.com/hk/java/technologies/downloads/#java21
NFTC是指:Oracle No-Fee Terms and Conditions许可 - 微软LTS发行版:https://learn.microsoft.com/zh-cn/java/openjdk/download-major-urls#openjdk-21-lts
- Eclipse发行版:https://adoptium.net/zh-CN/temurin/releases/
- OpenLogic发行版:https://www.openlogic.com/openjdk-downloads
我在生产环境用的当然还是Oracle的版本了。
JDK21的Demo项目下载
为方便后续问题解决,或快速创建新的JDK21项目,
写了一个基于JDK21+ spring-boot-starter3.3.1 + spring-cloud-starter-openfeign4.1.2的项目,放在github上,
有兴趣可以下载:
https://github.com/youbl/study/tree/master/jdk21-demo
升级过程出现的问题及解决
1、程序包javax.servlet.http不存在
在Controller里,一般会使用HttpServletRequest
和HttpServletResponse
,
在JDK8配套的SpringBoot2.*里,依赖的引用是:import javax.servlet.http.HttpServletRequest
而在SpringBoot3依赖的引用是import jakarta.servlet.http.HttpServletRequest
其它javax.servlet.http依赖都同样调整即可,注意要确认pom.xml添加了如下依赖:
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
</dependency>
注:代码中涉及javax的package,都要对应替换,下面举2个例子:
1.1、java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
如果代码报这个错,也是需要修改依赖,从javax 改为 jakarta,在pom.xml里引入:
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>3.0.1</version>
</dependency>
但是,我在对接阿里云时,它们的SDK报这个错,那这个就没法改pom了,因为它们的SDK代码里写死了javax,此时只能把javax的依赖加回来了,在pom.xml里,添加:
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
1.2、javax.validation包替换为jakarta.validation
同样如果使用了spring的validation,对应的依赖也要换成jakarta:
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
注:如果项目添加了下面的swagger依赖,那边自动集成了,就可以不需要自己加了。
1.3、jakarta的名字由来
当时觉得jakarta
这个命名很山寨,特意查了一下由来,发现是我自己山寨了,参考维基百科:https://zh.wikipedia.org/wiki/Jakarta%E9%A1%B9%E7%9B%AE
Jakarta的名称与印度尼西亚的首都雅加达(Jakarta)并无直接关系,
实际上它是根据Sun Microsystems公司当时讨论创建这个项目时的会议室命名的。
另外,jakarta.ee官网也解释了这个命名的由来:https://jakarta.ee/about/faq/
那里也是引用了维基百科的会议室来源说法,并在2018年2月进行了投票,64%的人支持Jakarta EE
这个命名。
而维基百科的Java_EE的页面没有找到相关说明:https://zh.wikipedia.org/wiki/Jakarta_EE
那为什么要改名呢?依据一些未经考证的说明,是因为Oracle把JavaEE移交给Eclipse基金会,同时不允许Eclipse基金会继续使用Java名号,所以才发起改名投票。
2、mybatis-plus升级
参考:https://github.com/baomidou/mybatis-plus
SpringBoot2用这个:
<!-- 这是SpringBoot2的 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
而升级到SpringBoot3,要用这个artifactId:mybatis-plus-spring-boot3-starter
<!-- 这是SpringBoot3的 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- 搭配的mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
3、mybatis-plus多数据源支持
参考:https://github.com/baomidou/dynamic-datasource
SpringBoot3要使用如下依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>4.3.1</version>
</dependency>
4、redis配置调整
在SpringBoot2里,redis的yml配置写法如下:
spring:
redis:
host: localhost
port: 6379
database: 15
password: 123456
升级到SpringBoot3后,redis的yml配置写法如下:
spring:
data:
redis:
host: localhost
port: 6379
database: 15
password: 123456
5、openfeign配置调整到spring.cloud下
在之前,feign的相关配置是这样的:
feign:
client:
config:
default:
logger-level: full
升级到SpringBoot3(对应spring-cloud-starter-openfeign4.1.2以上)后,相关的配置迁移了,参考: https://docs.spring.io/spring-cloud-openfeign/reference/spring-cloud-openfeign.html
新的配置是这样的:
spring:
cloud:
openfeign:
client:
config:
default:
logger-level: full
我在另一篇文章也做了代码断点调试来说明代码调用位置,参考:https://youbl.blog.csdn.net/article/details/109047987
6、升级到swagger3
在SpringBoot3项目的pom.xml里添加依赖,即可,启动项目使用地址:http://localhost:8080/swagger-ui.html
参考官网说明:https://springdoc.org/
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
</dependency>
如果想修改swagger api页面的介绍,可以在代码里定义如下Bean:
@Configuration
public class SwaggerApiConfig {
// 参考官网:https://springdoc.org/
@Bean
public OpenAPI springOpenAPI() {
Info info = new Info()
.title("beinet.cn jdk21 API demo文档")
.description("这是水边提供的jdk21代码demo,参考:https://youbl.blog.csdn.net/")
.version("0.0.1") // 版本号
.license(new License().name("Apache 2.0").url("https://youbl.blog.csdn.net/"));
ExternalDocumentation doc = new ExternalDocumentation()
.description("水边的Blog文档")
.url("https://youbl.blog.csdn.net/");
return new OpenAPI().info(info)
.externalDocs(doc);
}
}
注意:
如果项目报如下错误:
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'swaggerWebMvcConfigurer' defined in class path resource [org/springdoc/webmvc/ui/SwaggerConfig.class]: Unsatisfied dependency expressed through method 'swaggerWebMvcConfigurer' parameter 0: Error creating bean with name 'org.springdoc.core.properties.SwaggerUiConfigParameters': Failed to instantiate [org.springdoc.core.properties.SwaggerUiConfigParameters]: Constructor threw exception
这是因为内置的commons-lang3版本有问题,需要自定义版本,pom.xml参考:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.0.2</version>
<exclusions>
<exclusion>
<artifactId>commons-lang3</artifactId>
<groupId>org.apache.commons</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
6.1、swagger的新注解
相对于原来的swagger2,新的swagger3的注解全部换掉了,按官网说明,新旧注解对应关系如下:https://springdoc.org/#migrating-from-springfox
@Api → @Tag
@ApiIgnore → @Parameter(hidden = true) or @Operation(hidden = true) or @Hidden
@ApiImplicitParam → @Parameter
@ApiImplicitParams → @Parameters
@ApiModel → @Schema
@ApiModelProperty(allowEmptyValue = true) → @Schema(nullable = true)
@ApiModelProperty → @Schema
@ApiOperation(value = "foo", notes = "bar") → @Operation(summary = "foo", description = "bar")
@ApiParam → @Parameter
@ApiResponse(code = 404, message = "foo") → @ApiResponse(responseCode = "404", description = "foo")
但是实际我在应用中,发现也没有非常明确的对应关系,我的作法:
- 一般在Dto上,统一使用
@Schema
注解,如:
@Data
@Accessors(chain = true)
@Schema(description = "用户数据")
public class UsersDto {
@Schema(description = "用户id,主键")
private Long id;
@Size(max = 255)
@Schema(description = "用户名称")
private String name;
- 在Controller类上,使用
@Tag
注解,类里的Mapping接口上,使用@Operation
注解,如:
@RestController
@RequiredArgsConstructor
@Tag(name = "users", description = "用户增删改查接口类")
public class UsersController {
private final UsersService service;
@PostMapping("/users/all")
@Operation(summary = "用户列表", description = "用户列表查询接口")
public ResponseData<List<UsersDto>> findAll(@RequestBody UsersDto dto) {
return ResponseData.ok(service.search(dto));
}
效果如图,在页面上可以点击“Try it out”进行接口测试,类似于PostMan或Fiddler:
6.2、java.lang.NoSuchMethodError: ‘boolean org.apache.commons.lang3.math.NumberUtils.isCreatable(java.lang.String)’
如果swagger报如下错误:
java.lang.NoSuchMethodError: 'boolean org.apache.commons.lang3.math.NumberUtils.isCreatable(java.lang.String)'
at io.swagger.v3.core.jackson.ModelResolver.resolveMinimum(ModelResolver.java:1831) ~[swagger-core-jakarta-2.2.7.jar:2.2.7]
at io.swagger.v3.core.jackson.ModelResolver.resolveSchemaMembers(ModelResolver.java:2222) ~[swagger-core-jakarta-2.2.7.jar:2.2.7]
at io.swagger.v3.core.jackson.ModelResolver.resolveSchemaMembers(ModelResolver.java:2177) ~[swagger-core-jakarta-2.2.7.jar:2.2.7]
at io.swagger.v3.core.jackson.ModelResolver.resolve(ModelResolver.java:341) ~[swagger-core-jakarta-2.2.7.jar:2.2.7]
那么还是前面说的commons-lang3版本太低问题导致的,要按前面说的自定义升级方式指定高版本。
注:在实际项目中,出过这样一个问题:
- 在子模块的
<dependencies>
添加并指定了3.14.0的版本; - 在父模块使用
<dependencyManagement>
指定了3.4.0的版本; - 最终构建的结果会使用3.4.0,导致启动报错,当时查了挺久才发现问题。
7、No SLF4J providers were found.
启动报错:
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
通常是因为没有添加logging实现依赖,添加 spring-boot-starter-logging 引用解决:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
如果你的项目添加了spring-boot-starter-test,那应该不会报这个错。
注:也可以添加其它的依赖实现,格式会有点不好看就是了,参考:https://stackoverflow.com/questions/54652836/found-slf4j-api-dependency-but-no-providers-were-found
其它问题
import java.util.concurrent.TimeUnit 报错:
在idea中会标红,但是不影响使用,升级idea版本可以解决
参考:https://stackoverflow.com/questions/77551293/intellij-idea-jdk-21-issue-with-java-util-concurrent-package-timeunit-class
FeignClient 加 GetMapping,实际自动转POST发出
下面的feign定义,会自动转换为POST发请求,导出报错,路径不存在:
@GetMapping("/users")
ResponseData<UsersDto> pageIdentity(UsersDto dto);
因为默认情况下,feign会把复杂对象作为body进行提交,而http协议规范是不支持GET加body的,因此feign就自动转换为POST了。
解决办法,就是FeignClient不改,让调用的目标接口那边,改用PostMapping来接收body。
如果不改目标接口,在Feign的参数前加 @RequestParam 不能解决问题。
单元测试
步骤
以一个utils的工具类库demo项目为例,添加单元测试步骤:
1、test单元测试代码的目标结构:
- 对utils项目的pom.xml,添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
- 在src目录2. 新建子目录: test/java
注:test目录跟main目录是同级的 - 在test下新建子目录:resources,并新建文件:application.yml,内容参考:
spring:
application:
name: beinet-utils
profiles:
active: local
- 在test/java下新建package,必须跟main/java下的主java文件是相同package
- 在该新建的package下,新建
UtilsTestApplication
文件,内容参考:
注:因为spring-boot的单元测试要求要有@SpringBootApplication
定义的主类存在,而utils之类的项目一般没有
@SpringBootApplication(scanBasePackages = "cn.beinet")
public class UtilsTestApplication {
public static void main(String[] args) {
SpringApplication.run(UtilsTestApplication.class, args);
}
}
- 在该新建的package下,新建package为testHelper,再在其下新建单元测试类IpHelperTest.java 文件,内容参考:
package cn.beinet.core.utils.testHelper;
import cn.beinet.core.utils.UtilsTestApplication;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(classes = UtilsTestApplication.class)
public class IpHelperTest {
@Test
public void testDemo() {
var ts = System.currentTimeMillis();
Assert.assertTrue(ts > 1);
}
}
- OK,可以点击 testDemo左边的绿色小三角形,启动测试看看效果。
错误处理
java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=…) with your test
因为@SpringBootTest
注解,默认会在当前package下查找主类(即有@SpringBootApplication
注解的类)
找不到就会报错,要求在@SpringBootTest
注解里指定主类的位置,如:
@SpringBootTest(classes = UtilsTestApplication.class)
可以在test/java下的package下新建一个主类,参考上面步骤
提示:Java.lang.Exception: No runnable methods
这是因为@Test
注解用错了,
正确的Test注解全路径是 org.junit.Test
如果import导入了错误的package,用了 org.junit.jupiter.api.Test
就会报这个错误
测试类不支持注解:RequiredArgsConstructor
如果用了这个注解,会报错:
org.junit.runners.model.InvalidTestClassError: Invalid test class 'xxx.HandleFactoryTest':
1. Test class should have exactly one public zero-argument constructor
需要使用Bean时,在测试代码里,直接用 @Autowired
注解即可
Caused by: org.springframework.cloud.commons.ConfigDataMissingEnvironmentPostProcessor$ImportException: No spring.config.import set
这是因为项目依赖了配置中心,而yml中未指定配置中心的配置,需要在yml里指定一下,如:
spring:
config:
import: configserver:https://config-dev.beinet.cn
注意:其它必要的配置也不能遗漏,比如 spring.application.name
启动测试时,Autowired注解的类为null
如果单元测试依赖spring的Bean,则在该测试类上,必须添加注解:
@RunWith(SpringRunner.class)
如果不依赖Bean,则可以不需要该注解,以加快单元测试执行速度