SpringBoot项目升级到3.*,并由JDK8升级到JDK21
使用JDK21+ spring-boot-starter3.3.1 + spring-cloud-starter-openfeign4.1.2创建项目过程中碰到的问题小结,并提供项目Demo。
文章目录
- 技术选型说明
- JDK21的Demo项目下载
- 升级过程出现的问题及解决
- 其它问题
- 单元测试
-
- 步骤
- 错误处理
-
- 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
- 20241225更新
技术选型说明
前几个月搞新项目,做技术选型时,评估了一下,决定使用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,则可以不需要该注解,以加快单元测试执行速度
20241225更新
这2天,由于一个老项目内存占用太大,尝试从JDK11改造升级为JDK21,碰到了几个新问题,补充如下:
RestController实现里的约束与接口里的约束不一致导致的报错
报错参考:
beinet.cn.web.exception.SdkException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method AuthController#validCaptcha(String, String, String) redefines the configuration of AuthClient#validCaptcha(String, String, String).
原因是因为定义的接口:
@FeignClient(name = "beinet-auth", url = "${beinet.url.auth:beinet-auth}")
@Tag(name = "认证")
public interface AuthClient {
@Operation(summary = "校验验证码")
@GetMapping("/auth/captcha/valid")
ResponseData<Void> validCaptcha(@RequestParam String captcha, @RequestParam String scene, @RequestParam String emailOrPhone)
而对应的实现如下:
@Slf4j
@RestController
@Tag(name = "认证管理")
public class AuthController implements AuthClient {
@Override
public ResponseData<Void> validCaptcha(
@Parameter(description = "验证码") @RequestParam @Valid @NotBlank(message = "验证码不能为空") String captcha,
@Parameter(description = "验证码场景") @RequestParam @Valid @NotBlank(message = "场景不能为空") String scene,
@Parameter(description = "邮箱或电话") @RequestParam @Valid @NotBlank(message = "邮箱或电话不能为空") @Length(min = 1, max = 100, message = "邮箱或电话长度最大100个字符") String emailOrPhone) {
// 业务逻辑
}
可以看到:接口上没有NotBlank这种注解,实现类却有,在SpringBoot3是不允许的。
解决方法:
把@Valid @NotBlank这些注解,全部删除,或者全部迁移到AuthClient的接口方法定义上即可。
SpringBoot3默认不支持循环依赖
解决方法:
在需要循环依赖的属性上,添加@Lazy延迟加载的注解
或者实现一个工具类 SpringUtils.getBean(xxx.class)
建议:
代码上存在循环依赖,基本上都是设计问题,建议通过局部重构,避免出现循环依赖的场景。
kafkaTemplate.send.addCallback不能用了
旧项目存在这种代码:
kafkaTemplate.send(topic, payload).addCallback(o -> {
ContextUtils.setTraceId(traceId);
log.info("xxx");
}, ex -> {
ContextUtils.setTraceId(traceId);
log.info("推送到Kafka失败, {}", ex.getMessage(), ex);
});
在SpringBoot3会报错,不存在addCallback方法,
因为SpringBoot2中,send方法返回的是ListenableFuture<SendResult<K, V>>
而在SpringBoot3中,send方法返回的是CompletableFuture<SendResult<K, V>>
可以参考这里解决:https://stackoverflow.com/questions/77951258/replacing-listenablefuture-with-completablefuture-in-kafka-producer-consumer
我的解决方法就是直接删除addCallback代码,因为里面的逻辑只是记录日志,所以项目先跑起来再说。
没有CommonsMultipartFile
SpringBoot3之后没有这个类了:org.springframework.web.multipart.commons.CommonsMultipartFile
我的解决方法就是去低版本里,把这个类复制到新的项目里,如:
package beinet.cn.utils.multipart;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* 新类
* @author youbl
* @since 2024/12/24 11:45
*/
public class CommonsMultipartFile implements MultipartFile, Serializable {
protected static final Log logger = LogFactory.getLog(CommonsMultipartFile.class);
private final FileItem fileItem;
private final long size;
private boolean preserveFilename = false;
/**
* Create an instance wrapping the given FileItem.
* @param fileItem the FileItem to wrap
*/
public CommonsMultipartFile(FileItem fileItem) {
this.fileItem = fileItem;
this.size = this.fileItem.getSize();
}
/**
* Return the underlying {@code org.apache.commons.fileupload.FileItem}
* instance. There is hardly any need to access this.
*/
public final FileItem getFileItem() {
return this.fileItem;
}
/**
* Set whether to preserve the filename as sent by the client, not stripping off
* path information in {@link CommonsMultipartFile#getOriginalFilename()}.
* <p>Default is "false", stripping off path information that may prefix the
* actual filename e.g. from Opera. Switch this to "true" for preserving the
* client-specified filename as-is, including potential path separators.
* @since 4.3.5
* @see #getOriginalFilename()
* @see CommonsMultipartResolver#setPreserveFilename(boolean)
*/
public void setPreserveFilename(boolean preserveFilename) {
this.preserveFilename = preserveFilename;
}
@Override
public String getName() {
return this.fileItem.getFieldName();
}
@Override
public String getOriginalFilename() {
String filename = this.fileItem.getName();
if (filename == null) {
// Should never happen.
return "";
}
if (this.preserveFilename) {
// Do not try to strip off a path...
return filename;
}
// Check for Unix-style path
int unixSep = filename.lastIndexOf('/');
// Check for Windows-style path
int winSep = filename.lastIndexOf('\\');
// Cut off at latest possible point
int pos = Math.max(winSep, unixSep);
if (pos != -1) {
// Any sort of path separator found...
return filename.substring(pos + 1);
} else {
// A plain name
return filename;
}
}
@Override
public String getContentType() {
return this.fileItem.getContentType();
}
@Override
public boolean isEmpty() {
return (this.size == 0);
}
@Override
public long getSize() {
return this.size;
}
@Override
public byte[] getBytes() {
if (!isAvailable()) {
throw new IllegalStateException("File has been moved - cannot be read again");
}
byte[] bytes = this.fileItem.get();
return (bytes != null ? bytes : new byte[0]);
}
@Override
public InputStream getInputStream() throws IOException {
if (!isAvailable()) {
throw new IllegalStateException("File has been moved - cannot be read again");
}
InputStream inputStream = this.fileItem.getInputStream();
return (inputStream != null ? inputStream : StreamUtils.emptyInput());
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
if (!isAvailable()) {
throw new IllegalStateException("File has already been moved - cannot be transferred again");
}
if (dest.exists() && !dest.delete()) {
throw new IOException(
"Destination file [" + dest.getAbsolutePath() + "] already exists and could not be deleted");
}
try {
this.fileItem.write(dest);
LogFormatUtils.traceDebug(logger, traceOn -> {
String action = "transferred";
if (!this.fileItem.isInMemory()) {
action = (isAvailable() ? "copied" : "moved");
}
return "Part '" + getName() + "', filename '" + getOriginalFilename() + "'" +
(traceOn ? ", stored " + getStorageDescription() : "") +
": " + action + " to [" + dest.getAbsolutePath() + "]";
});
} catch (FileUploadException ex) {
throw new IllegalStateException(ex.getMessage(), ex);
} catch (IllegalStateException | IOException ex) {
// Pass through IllegalStateException when coming from FileItem directly,
// or propagate an exception from I/O operations within FileItem.write
throw ex;
} catch (Exception ex) {
throw new IOException("File transfer failed", ex);
}
}
@Override
public void transferTo(Path dest) throws IOException, IllegalStateException {
if (!isAvailable()) {
throw new IllegalStateException("File has already been moved - cannot be transferred again");
}
FileCopyUtils.copy(this.fileItem.getInputStream(), Files.newOutputStream(dest));
}
/**
* Determine whether the multipart content is still available.
* If a temporary file has been moved, the content is no longer available.
*/
protected boolean isAvailable() {
// If in memory, it's available.
if (this.fileItem.isInMemory()) {
return true;
}
// Check actual existence of temporary file.
if (this.fileItem instanceof DiskFileItem) {
return ((DiskFileItem) this.fileItem).getStoreLocation().exists();
}
// Check whether current file size is different than original one.
return (this.fileItem.getSize() == this.size);
}
/**
* Return a description for the storage location of the multipart content.
* Tries to be as specific as possible: mentions the file location in case
* of a temporary file.
*/
public String getStorageDescription() {
if (this.fileItem.isInMemory()) {
return "in memory";
} else if (this.fileItem instanceof DiskFileItem) {
return "at [" + ((DiskFileItem) this.fileItem).getStoreLocation().getAbsolutePath() + "]";
} else {
return "on disk";
}
}
}
更多推荐


所有评论(0)