01_学习使用javax_ws_rs_上传文件

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 结语

  感谢阅读~

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值