当前位置: 首页 > article >正文

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

  感谢阅读~


http://www.kler.cn/a/153924.html

相关文章:

  • 运维面试题.云计算面试题之三ELK
  • 移门缓冲支架的工作原理
  • vue 常用特性 ( 计算属性 | 侦听器 | 过滤器 )
  • Linux的目录结构
  • 内容占位符:Kinetic Loader HTML+CSS 使用CSS制作三角形原理
  • R语言基础入门详解
  • jq h5 图片上传回显
  • vue2 -- 封装 echarts 基础组件
  • hive两张表实现like模糊匹配关联
  • 基于OGG实现Oracle实时同步MySQL
  • 人工智能_AI服务器安装清华开源_CHATGLM大语言模型_GLM-6B安装部署_人工智能工作笔记0092
  • TCP_握手+挥手过程状态变化分析
  • 【MVP矩阵】投影矩阵推导与实现
  • 递归实现排列型枚举
  • redisson分布式锁
  • 2015年五一杯数学建模B题空气污染问题研究解题全过程文档及程序
  • 基于JavaWeb+SSM+Vue校园综合服务小程序系统的设计和实现
  • C语言每日一题(43)旋转链表
  • FWFT-FIFO的同步和异步verilog代码
  • Swoole的多进程模块
  • 22、DS1302实时时钟
  • YOLOv8 第Y7周 水果识别
  • NXP iMX8M Plus Qt5 双屏显示
  • 【开源】基于JAVA语言的校园电商物流云平台
  • 匿名结构体类型、结构体的自引用、结构体的内存对齐以及结构体传参
  • Git多库多账号本地SSH连接配置方法