01_学习使用javax_ws_rs_上传文件
文章目录
- 1 前言
- 2 Maven 依赖
- 3 上传接口
- 4 如何解析 MultipartFormDataInput
- 5 结语
1 前言
使用 Spring MVC 来处理文件上传,想必是大家耳熟能详的了,如下代码:
@ResponseBody
@PostMapping("/upload")
public String upload(@RequestPart("file") MultipartFile file) throws IOException {
appService.upload(file);
return "Success";
}
😜 但是现在,如果我们不使用 Spring MVC , 而是使用 javax.ws.rs
下的注解,该如何实现文件上传呢?
2 Maven 依赖
<properties>
<jboss.resteasy.version>3.6.3.Final</jboss.resteasy.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>${jboss.resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
<version>${jboss.resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxb-provider</artifactId>
<version>${jboss.resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>${jboss.resteasy.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-netty</artifactId>
<version>${jboss.resteasy.version}</version>
</dependency>
<!-- resteasy 文件上传的依赖开始 -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
<version>${jboss.resteasy.version}</version>
</dependency>
<!-- resteasy 文件上传的依赖结束 -->
</dependencies>
3 上传接口
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@Path("/test")
public interface TestController {
@POST
@Path("upload")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA)
JsonResult<String> upload(MultipartFormDataInput input);
}
4 如何解析 MultipartFormDataInput
从 MultipartFormDataInput
中应当可以解析出文件 或者其他 form-data 的字段。通过下面的 getDtoFromMultipartFormDataInput()
方法,我们就可以把多部件转换为指定类型的 dto 了。然后就可以进行 DAO 层操作了。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import javax.ws.rs.core.MultivaluedMap;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Slf4j
public class TestService {
/**
* multipart-form/data 的文件的 key 统一为 file
*/
private static final String FORM_FILE_KEY = "file";
/**
* 把 {@link MultipartFormDataInput} 转换为指定类型的 dto
*
* @param input 多部件上传的参数, 里面可能有文件, 也可能只是普通的 key-value
* @param clazz dto 的类型
* @return dto
*/
public <T> T getDtoFromMultipartFormDataInput(MultipartFormDataInput input, Class<? extends T> clazz) {
T dto;
try {
dto = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
log.error("[把多部件转换为 dto] 无法实例化 dto, 类名: {}", clazz.getName());
throw new RuntimeException("[把多部件转换为 dto] 无法实例化 dto, 类名: " + clazz.getName(), e);
}
Map<String, List<InputPart>> formDataMap = input.getFormDataMap();
// 处理多部件中的文件, 可能需要解析文件, 也可能不需要, 全看 dto 里面有没有 file 字段
this.handleFileFieldIfNeed(clazz, formDataMap, dto);
// 解析 multipart/form-data 的其他字段
this.handleNonFileFields(clazz, formDataMap, dto);
return dto;
}
/**
* 处理 dto 中 非文件类型的字段
*
* @param clazz dto 的类型
* @param formDataMap 多部件的内容
* @param dto dto 实例, new 出来的
* @param <T> 泛型, dto 的类型
*/
private <T> void handleNonFileFields(Class<? extends T> clazz, Map<String, List<InputPart>> formDataMap, T dto) {
Field[] dtoFields = clazz.getDeclaredFields();
for (Field dtoField : dtoFields) {
String fieldName = dtoField.getName();
if (!"file".equals(fieldName)) {
// 处理非文件的字段
List<InputPart> inputParts = formDataMap.get(fieldName);
if (CollectionUtils.isEmpty(inputParts)) {
log.warn("[把多部件转换为 dto] dto 里面不存在 multipart/form-data 中的字段: {}", fieldName);
} else {
// 只取第一个
InputPart inputPart = inputParts.get(0);
try {
Object dtoFieldValue = inputPart.getBody(dtoField.getType(), null);
boolean accessible = dtoField.isAccessible();
if (!accessible) {
dtoField.setAccessible(true);
}
dtoField.set(dto, dtoFieldValue);
if (!accessible) {
dtoField.setAccessible(false);
}
} catch (IOException | IllegalAccessException e) {
log.error("[把多部件转换为 dto] dto 字段: " + fieldName + " 无法从 form-data 中获取值", e);
throw new RuntimeException("[把多部件转换为 dto] dto 字段: " + fieldName + " 无法从 form-data 中获取值", e);
}
}
}
}
}
/**
* 处理多部件中的文件, 可能需要解析文件, 也可能不需要, 全看 dto 里面有没有 file 字段
*
* @param clazz dto 的类型
* @param formDataMap 多部件的内容
* @param dto dto 实例, new 出来的
* @param <T> 泛型, dto 的类型
*/
private <T> void handleFileFieldIfNeed(Class<? extends T> clazz, Map<String, List<InputPart>> formDataMap, T dto) {
try {
Field fileField = clazz.getDeclaredField("file");
if (File.class.equals(fileField.getType())) {
// 如果 dto 里面有 file 这个字段, 而且 file 字段的类型是 java.io.File, 那么就开始解析
// multipart/form-data 的内容, 解析出一个文件出来
List<InputPart> inputParts = formDataMap.get(FORM_FILE_KEY);
if (CollectionUtils.isEmpty(inputParts)) {
throw new RuntimeException("[把多部件转换为 dto] 上传文件的 form-data 的 key 应该为 file");
}
// 一个 key "file", 只对应一个 文件
InputPart inputPart = inputParts.get(0);
// 解析文件名
MultivaluedMap<String, String> headers = inputPart.getHeaders();
String filename = this.getFilename(headers)
.orElseThrow(() -> new RuntimeException("[把多部件转换为 dto] 解析文件名称失败"));
// 解析文件流
try (InputStream is = inputPart.getBody(InputStream.class, null)) {
// 先生成本地的临时文件
File file = this.stream2file(is, filename);
// 然后把这个文件设置到 dto 的 file 字段里面
boolean fileFieldAccessible = fileField.isAccessible();
if (!fileFieldAccessible) {
fileField.setAccessible(true);
}
fileField.set(dto, file);
if (!fileFieldAccessible) {
fileField.setAccessible(false);
}
} catch (IOException e) {
log.error("[把多部件转换为 dto] 获取文件输入流 或者把此输入流转换为字节数组失败", e);
throw new RuntimeException("[把多部件转换为 dto] 获取文件输入流 或者把此输入流转换为字节数组失败");
} catch (IllegalAccessException e) {
log.error("[把多部件转换为 dto] 把 File 设置到 dto 的 file 字段时, 失败", e);
throw new RuntimeException("[把多部件转换为 dto] 把 File 设置到 dto 的 file 字段时, 失败");
}
}
} catch (NoSuchFieldException e) {
// 没有名称为 file 的字段
log.warn("[把多部件转换为 dto] dto 里面没有 file 字段. class:{}", clazz.getName());
}
}
/**
* 从 http 请求头中, 获取文件的名称
*
* @param headers http 请求头
* @return 文件的名称, 可能为空
*/
private Optional<String> getFilename(MultivaluedMap<String, String> headers) {
String[] contentDispositionArr = headers.getFirst("Content-Disposition").split(";");
for (String contentDisposition : contentDispositionArr) {
if (contentDisposition.trim().startsWith("filename")) {
String[] filenameArr = contentDisposition.split("=");
String filename = filenameArr[1].trim().replaceAll("\"", "");
if (!StringUtils.hasText(filename)) {
// 文件名为空的话, 也是不可以的
return Optional.empty();
}
String finalFilename = this.urlDecodeFilename(filename);
return Optional.ofNullable(finalFilename);
}
}
return Optional.empty();
}
/**
* 把字节输入流 转换为 临时文件.
* 这些临时文件要记得及时清除
*
* @param is 字节输入流
* @param filename 文件名
* @return 临时文件
*/
private File stream2file(InputStream is, String filename) {
File file = this.createTempFile(filename);
try (FileOutputStream fos = new FileOutputStream(file)) {
IOUtils.copy(is, fos);
} catch (IOException e) {
log.error("把输入流的内容拷贝到文件输出流 失败", e);
}
return file;
}
/**
* 创建临时文件
*
* @param filename 临时文件名, 不包含路径
* @return 临时文件
*/
public File createTempFile(String filename) {
// 统一的临时文件上传目录. 目录加了日期和uuid的区分
String tmpParentDirPath = this.getTmpFileParentDirPath()
+ File.separator;
File file = new File(tmpParentDirPath + filename);
File parentDir = file.getParentFile();
if (!parentDir.exists()) {
// 生成父目录
//noinspection ResultOfMethodCallIgnored
parentDir.mkdirs();
}
if (!file.exists()) {
try {
//noinspection ResultOfMethodCallIgnored
file.createNewFile();
} catch (IOException e) {
log.error("创建临时文件失败, 文件名: {}", file.getAbsolutePath());
throw new RuntimeException("创建临时文件失败, 文件名: " + file.getAbsolutePath(), e);
}
}
return file;
}
/**
* 获取临时文件所在的父目录
*/
private String getTmpFileParentDirPath() {
// 父目录的格式是 D:/年月日/uuid
return "D:" + File.separator
+ DateTimeUtil.formatString(new Date(), "yyyy-MM-dd")
+ File.separator
+ UUID.randomUUID().toString().replaceAll("-", "");
}
}
这个 dto 可以是:
import java.io.File;
@Data
public class MyTestDto {
private File file;
private String queryParamOne;
private String queryParamTwo;
}
5 结语
感谢阅读~