[ファイル] SpringBootを使用してファイルをアップロードおよびダウンロードします

ファイルのアップロードとダウンロードは実際のプロジェクトでもよく使われるので、今日はここでまとめておきます。

このブログは、ファイルのアップロード/ダウンロード機能を完了することだけを目的としているわけではありません。さらに重要なのは、ログ記録、入力/戻りパラメーターの検証、単一メソッドの責任などのコーディングを標準化することです。

開発環境:

1.IDEA 2020.2
2.Maven 3.6.0
3.SpringBoot 2.0.0.RELEASE

1. 単一ファイルのアップロード

新しい SpringBoot プロジェクトを作成します。プロジェクトの構造図は次のとおりです。

ここに画像の説明を挿入します
POM の依存関係:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>
<dependency>
	<groupId>joda-time</groupId>
	<artifactId>joda-time</artifactId>
	<version>2.9.7</version>
</dependency>

アプリケーション.yml

spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB       # 单个文件上传的最大上限
      max-request-size: 10MB    # 一次请求总大小上限
file:                           # 文件上传路径
  uploadPath: E:/upload

次に、コードで正式に実装されました~~

ファイルアップロードインターフェースFileController

@RestController
@RequestMapping("/file")
public class FileController {
    
    

    @Autowired
    private FileService fileService;

    // 上传单个文件
    @PostMapping("/upload")
    public ResultVo<String> uploadFile(@RequestParam("file") MultipartFile file) {
    
    
        return fileService.uploadFile(file);
    }

}

FileService

public interface FileService {
    
    
    // 上传文件
    ResultVo<String> uploadFile(MultipartFile file);
    
}

FileServiceImpl

@Service
@Slf4j
public class FileServiceImpl implements FileService {
    
    

    // 上传文件
    @Override
    public ResultVo<String> uploadFile(MultipartFile file) {
    
    
        log.info("【文件上传】进入到文件上传方法");
        // 1.参数校验
        if (null == file || file.isEmpty()) {
    
    
            log.error("【文件上传】文件为空!");
            throw new ParamErrorException();
        }
        // 2.上传文件
        ResultVo<String> resultVo = FileUtil.uploadFile(file);
        return resultVo;
    }

}

例証します:

  1. ロギングを行う
  2. パラメータの確認
  3. カスタム例外ParamErrorException処理
  4. 戻り結果の統合カプセル化ResultVo
  5. 単一責任。ツール クラスを使用してファイル アップロード操作を実行します。FileUtil#uploadFile()

1》、カスタム例外ParamErrorException

@Data
public class ParamErrorException extends RuntimeException {
    
    

    // 错误码
    private Integer code;

    // 错误消息
    private String msg;

    public ParamErrorException() {
    
    
        this(ResultCodeEnum.PARAM_ERROR.getCode(), ResultCodeEnum.PARAM_ERROR.getMessage());
    }
    public ParamErrorException(String msg) {
    
    
        this(ResultCodeEnum.PARAM_ERROR.getCode(), msg);
    }
    public ParamErrorException(Integer code, String msg) {
    
    
        super(msg);
        this.code = code;
        this.msg = msg;
    }

}

ResultCodeEnum

カスタム列挙クラスはカスタム例外でも使用されます。

@Getter
public enum ResultCodeEnum {
    
    

    SUCCESS(200, "成功")
    ,
    ERROR(301, "错误")
    ,
    PARAM_ERROR(303, "参数错误")
    ,
    FILE_NOT_EXIST(304, "文件不存在")
    ,
    CLOSE_FAILD(305, "关闭流失败")
    ;

    private Integer code;
    private String message;

    ResultCodeEnum(Integer code, String message) {
    
    
        this.code = code;
        this.message = message;
    }

}

FileExceptionControllerAdvice

ではSpringBoot、スローされた例外をそれぞれ処理するのではなくtry {} catch(Exception e) {}、アノテーションを使用して@RestControllerAdvice例外を均一に処理します。

@RestControllerAdvice
public class FileExceptionControllerAdvice {
    
    

    // 处理文件为空的异常
    @ExceptionHandler(ParamErrorException.class)
    public ResultVo<String> fileExceptionHandler(ParamErrorException exception) {
    
    
        return ResultVoUtil.error(exception.getCode(), exception.getMsg());
    }

}

2》. カプセル化された結果の統一された返却ResultVo

@Data
public class ResultVo<T> {
    
    

    // 错误码
    private Integer code;

    // 提示信息
    private String msg;

    // 返回的数据
    private T data;
    
    // 判断是否成功
	public boolean checkSuccess() {
    
    
        return ResultCodeEnum.SUCCESS.getCode().equals(this.code);
    }

}

ResultVoUtil

ツールクラスをカプセル化して成功/失敗を返す

public class ResultVoUtil {
    
    

    public static ResultVo success() {
    
    
        return success(null);
    }
    public static ResultVo success(Object object) {
    
    
        ResultVo result = new ResultVo();
        result.setCode(ResultCodeEnum.SUCCESS.getCode());
        result.setMsg("成功");
        result.setData(object);
        return result;
    }
    public static ResultVo success(Integer code, Object object) {
    
    
        return success(code, null, object);
    }
    public static ResultVo success(Integer code, String msg, Object object) {
    
    
        ResultVo result = new ResultVo();

        result.setCode(code);
        result.setMsg(msg);
        result.setData(object);
        return result;
    }

    public static ResultVo error(String msg) {
    
    
        ResultVo result = new ResultVo();
        result.setCode(ResultCodeEnum.ERROR.getCode());
        result.setMsg(msg);
        return result;
    }
    public static ResultVo error(Integer code, String msg) {
    
    
        ResultVo result = new ResultVo();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }

}

3》、ファイルのアップロード

@Slf4j
public class FileUtil {
    
    

    private static FileConfig fileConfig = ApplicationContextHolder.getContext().getBean(FileConfig.class);

    // 下划线
    public static final String UNDER_LINE = "_";

    // 上传文件
    public static ResultVo<String> uploadFile(MultipartFile file) {
    
    
        // 1.获取一个新的文件名
        String newFileName = getNewFileName(file);
        if (StringUtil.isBlank(newFileName)) {
    
    
            log.error("【上传文件】转换文件名称失败");
            return ResultVoUtil.error("【上传文件】转换文件名称失败");
        }
        // 2.获取文件上传路径
        String uploadPath = fileConfig.getUploadPath();
        if (StringUtil.isBlank(uploadPath)) {
    
    
            log.error("【上传文件】获取文件上传路径失败");
            return ResultVoUtil.error("【上传文件】获取文件上传路径失败");
        }
        uploadPath = uploadPath + File.separator +  DateUtil.getCurrentDate();
        // 3.生成上传目录
        File uploadDir = mkdirs(uploadPath);
        if (!uploadDir.exists()) {
    
    
            log.error("【上传文件】生成上传目录失败");
            return ResultVoUtil.error("【上传文件】生成上传目录失败");
        }
        // 4.文件全路径
        String fileFullPath = uploadPath + File.separator + newFileName;
        log.info("上传的文件:" + file.getName() + "," + file.getContentType() + ",保存的路径为:" + fileFullPath);
        try {
    
    
            // 5.上传文件
            doUploadFile(file, fileFullPath);
        } catch (IOException e) {
    
    
            log.error("【上传文件】上传文件报IO异常,异常信息为{}", e.getMessage());
            return ResultVoUtil.error(e.getMessage());
        }
        return ResultVoUtil.success(fileFullPath);
    }
    
}

例証します:

  1. 文字列ツール クラスをカプセル化して、StringUtil文字列が空かどうかを判断します。
  2. ファイルのアップロード パスを取得します。String uploadPath = fileConfig.getUploadPath();このコードは非常に興味深いです。
  3. DateUtil.getCurrentDate()現在の日付を取得するメソッド。

StringUtil: 文字列ツールクラス

@Component
public class StringUtil {
    
    

    // 判断字符串是否为空
    public static boolean isBlank(String content) {
    
    
        if (null == content || "".equals(content)) {
    
    
            return true;
        }
        return false;
    }

}

ファイルのアップロード パスを取得するために、なぜこの方法を記述する必要があるのでしょうか? String UploadPath = fileConfig.getUploadPath();

よく考えて、application.ymlファイル内にファイルのアップロード パスを構成します。

file:
  uploadPath: E:/upload

では、この値を取得するにはどうすればよいでしょうか?

@Valueアノテーションと@ConfigurationPropertiesアノテーションの2つの考え方があります。

ここでは 2 番目のオプションを選択しました。

FileConfig親切:

@Data
@Component
@ConfigurationProperties(prefix = "file")
public class FileConfig {
    
    

    // 上传路径
    private String uploadPath;

}

このようにして、FileConfigクラス内のプロパティuploadPathに値を割り当てることができます。次に、fileConfig.getUploadPath()経由でそれを取得できます。

ただし、このメソッドは静的メソッドでは機能しません。今すぐ:

public class FileUtil {
    
    
    @Autowired
    private FileConfig fileConfig;
	
	public static ResultVo<String> uploadFile(MultipartFile file) {
    
    
		...
		String uploadPath = fileConfig.getUploadPath();
		...
	}
}

この種のコーディングではコンパイル期間を過ぎることもできません。なぜ?

静的ではないプロパティ/メソッドを静的メソッドで呼び出すことができないためです。

したがって、解決策は次のとおりです。

private static FileConfig fileConfig = ApplicationContextHolder.getContext().getBean(FileConfig.class);

ApplicationContextHolder: 静的変数には Spring ApplicationContext が保存され、いつでもどこでも任意のコードから取り出すことができますApplicaitonContext

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
    
    

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    
    
        context = applicationContext;
    }

    public static ApplicationContext getContext() {
    
    
        return context;
    }

    public static Object getBean(String name) {
    
    
        return context != null ? context.getBean(name) : null;
    }

    public static <T> T getBean(Class<T> clz) {
    
    
        return context != null ? context.getBean(clz) : null;
    }

    public static <T> T getBean(String name, Class<T> clz) {
    
    
        return context != null ? context.getBean(name, clz) : null;
    }

    public static void addApplicationListenerBean(String listenerBeanName) {
    
    
        if (context != null) {
    
    
            ApplicationEventMulticaster applicationEventMulticaster = (ApplicationEventMulticaster)context.getBean(ApplicationEventMulticaster.class);
            applicationEventMulticaster.addApplicationListenerBean(listenerBeanName);
        }
    }

}

DateUtil:日付ツールクラス:

@Component
public class DateUtil {
    
    

    // 获取当前时间
    public static String getCurrentTime() {
    
    
        DateTime now = new DateTime();
        return now.toString(DateConstant.DEFAULT_FORMAT_PATTERN);
    }

    // 获取当前日期
    public static String getCurrentDate() {
    
    
        LocalDate localDate = new LocalDate();
        return localDate.toString();
    }
    
}

DateConstant:日付定数クラス

public interface DateConstant {
    
    

    // 默认的日期格式化格式
    String DEFAULT_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss";

}

今回はjoda-timeこのコンポーネントを選択しました。このコンポーネントは部門のお偉いさんから勧められたので、ここで簡単にお勧めします。

getNewFileName(MultipartFile)方法: アップロードされたファイルを新しいファイル名に変換する

// 将上传的文件转换为一个新的文件名
public static String getNewFileName(MultipartFile file) {
    
    
    // 1.获取上传的文件名称(包含后缀。如:test.jpg)
    String originalFilename = file.getOriginalFilename();
    log.info("【上传文件】上传的文件名为{}", originalFilename);
    // 2.以小数点进行分割
    String[] split = originalFilename.split("\\.");
    String newFileName = null;
    if (null == split || split.length == 0) {
    
    
        return null;
    }
    StringBuilder builder = new StringBuilder();
    if (1 == split.length) {
    
    
        // 3.此文件无后缀
        newFileName = builder.append(originalFilename).append(UNDER_LINE).append(System.nanoTime()).toString();
        return newFileName;
    }
    // 4.获取文件的后缀
    String fileSuffix = split[split.length - 1];
    for (int i = 0; i < split.length - 1; i++) {
    
    
        builder.append(split[i]);
        if (null != split[i + 1] && "" != split[i + 1]) {
    
    
            builder.append(UNDER_LINE);
        }
    }
    newFileName = builder.append(System.nanoTime()).append(".").append(fileSuffix).toString();
    return newFileName;
}

例証します:

  1. アップロードされたファイルにサフィックス (「.」で区別) がない場合は、アンダースコア「_」を直接連結し、ファイル名の重複を避けてからタイムスタンプを連結します。
  2. アップロードされたファイルにサフィックスが付いている場合は、ファイル名の「.」をアンダースコア「_」に変換し、タイムスタンプを結合します。

mkdirs(String)方法: 対応するディレクトリを生成する

// 生成相应的目录
public static File mkdirs(String path) {
    
    
    File file = new File(path);
    if(!file.exists() || !file.isDirectory()) {
    
    
        file.mkdirs();
    }
    return file;
}

doUploadFile(MultipartFile, String)方法: ファイルをアップロードする

// 上传文件
public static void doUploadFile(MultipartFile file, String path) throws IOException {
    
    
    Streams.copy(file.getInputStream(), new FileOutputStream(path), true);
}

明らかに、これは流れの変換です。

ファイルをアップロードするには、次のようにさまざまな方法があります。

public static void doUploadFile(MultipartFile file, String path) throws IOException {
    
    
    // 法一:
    Streams.copy(file.getInputStream(), new FileOutputStream(path), true);

    // 法二: 通过MultipartFile#transferTo(File)
    // 使用此方法保存,必须要绝对路径且文件夹必须已存在,否则报错
    //file.transferTo(new File(path));

    // 法三:通过NIO将字节写入文件
    //Path filePath = Paths.get(path);
    //Files.write(filePath, file.getBytes());

    // 法四:
    /*try (InputStream in = file.getInputStream();
         FileOutputStream out = new FileOutputStream(path)) {
        IOUtils.copy(in, out);
    } catch (Exception e) {
        log.error("【上传文件】上传文件失败,失败信息为:{}", e.getMessage());
    }*/

    // 法五:
    /*InputStream in = file.getInputStream();
    OutputStream out = new FileOutputStream(path);
    int len = 0;
    byte[] bytes = new byte[1024];
    while ((len = in.read(bytes)) != -1) {
        out.write(bytes, 0, len);
    }
    in.close();
    out.close();*/

    // 法六:
    /*byte[] bytes = file.getBytes();
    OutputStream out = new FileOutputStream(path);
    out.write(bytes);
    out.close();*/
}

ここで、筆者はどの方法を使用すればよいのかは知りません。これらの方法でファイルのアップロード機能が実現できることだけを知っています。しかし、それらには確かに長所と短所があります。

2. 複数のファイルのアップロード

複数ファイルのアップロードと単一ファイルのアップロードは基本的に同じです。ただし、複数のファイルをアップロードするためのバックグラウンド インターフェイスは、配列を使用して受信し、ループ内でファイル アップロード メソッドを呼び出します。コードは次のとおりです。

FileController: インターフェースを追加します

@PostMapping("/uploadFiles")
public ResultVo uploadFiles(@RequestParam("files") MultipartFile[] files) {
    
    
    return fileService.uploadFiles(files);
}

FileServiceImpl:メソッドを追加します:

@Override
public ResultVo uploadFiles(MultipartFile[] files) {
    
    
    log.info("【批量上传】进入到批量上传文件");
    if (null == files || files.length == 0) {
    
    
        log.error("【批量上传】上传的文件为空,files={}", files);
        throw new ParamErrorException();
    }
    List<MultipartFile> multipartFiles = Arrays.asList(files);
    // 1.校验是否有空文件
    List<String> emptyFileNames = new ArrayList<>();
    List<MultipartFile> needUploadFiles = new ArrayList<>();
    int count = 0;
    for (MultipartFile file : multipartFiles) {
    
    
        if (null == file) {
    
    
            count++;
            continue;
        }
        if (file.isEmpty()) {
    
    
            emptyFileNames.add(file.getOriginalFilename());
            count++;
            continue;
        }
        needUploadFiles.add(file);
    }
    if (count == multipartFiles.size()) {
    
    
        log.error("【批量上传】批量上传的文件为空,无法正确上传");
        return ResultVoUtil.error("批量上传的文件为空,无法正确上传");
    }
    if (CollectionUtil.isNotEmpty(emptyFileNames)) {
    
    
        log.info("【批量上传】一共上传了{}个文件,其中,空文件数为{},空文件名分别是:{}", multipartFiles.size(), count, emptyFileNames);
    } else {
    
    
        log.info("【批量上传】一共上传了{}个文件", multipartFiles.size());
    }
    // 2.批量上传文件
    List<String> uploadFailFileNames = new ArrayList<>(needUploadFiles.size());
    needUploadFiles.forEach((file) -> {
    
    
        ResultVo<String> resultVo = FileUtil.uploadFile(file);
        // 如果没有上传成功
        if (!resultVo.checkSuccess()) {
    
    
            uploadFailFileNames.add(file.getName());
        }
    });
    if (CollectionUtil.isNotEmpty(uploadFailFileNames)) {
    
    
        log.error("一共上传了{}个文件,其中上传失败的文件数为{},文件名分别为:{}", needUploadFiles.size(), uploadFailFileNames.size(), uploadFailFileNames);
        return ResultVoUtil.success("一共上传了" + needUploadFiles.size() + "个文件,其中上传失败的文件数为" + uploadFailFileNames.size() + ",文件名分别为:" + uploadFailFileNames);
    }
    log.info("批量上传文件成功");
    return ResultVoUtil.success();
}

CollectionUtil

public class CollectionUtil {
    
    

    public static boolean isNotEmpty(Collection<?> coll) {
    
    
        return !isEmpty(coll);
    }

    public static boolean isEmpty(Collection<?> coll) {
    
    
        return coll == null || coll.isEmpty();
    }
}

POSTMAN を使用してインターフェイスを呼び出す場合は、複数のファイルをアップロードします。
ここに画像の説明を挿入します
自分でテストできます。

3. ファイルのダウンロード

パスに基づいてファイルをダウンロードします。ここでは、便宜上、このパスをインターフェイス経由で渡します。

FileController

@PostMapping("/download")
public ResultVo<String> downloadFile(@RequestParam("filePath") String filePath, final HttpServletResponse response) {
    
    
    return fileService.downloadFile(filePath, response);
}

FileServiceImpl

@Override
public ResultVo<String> downloadFile(String filePath, HttpServletResponse response) {
    
    
    File file = new File(filePath);
    // 1.参数校验
    if (!file.exists()) {
    
    
        log.error("【下载文件】文件路径{}不存在", filePath);
        return ResultVoUtil.error("文件不存在");
    }
    // 2.下载文件
    log.info("【下载文件】下载文件的路径为{}", filePath);
    return FileUtil.downloadFile(file, response);
}

FileUtil:ダウンロードファイル

public static ResultVo<String> downloadFile(File file, HttpServletResponse response) {
    
    
    try {
    
    
        // 1.设置响应头
        setResponse(file, response);
    } catch (UnsupportedEncodingException e) {
    
    
        log.error("文件名{}不支持转换为字符集{}", file.getName(), "UTF-8");
        return ResultVoUtil.error(e.getMessage());
    }
    // 2.下载文件
    return doDownLoadFile(file, response);
}

setResponse(): レスポンスヘッダーを設定します

public static void setResponse(File file, HttpServletResponse response) throws UnsupportedEncodingException {
    
    
    // 清空response
    response.reset();
    response.setCharacterEncoding("UTF-8");
    // 返回给客户端类型,任意类型
    response.setContentType("application/octet-stream");
    // Content-Disposition的作用:告知浏览器以何种方式显示响应返回的文件,用浏览器打开还是以附件的形式下载到本地保存
    // attachment表示以附件方式下载 inline表示在线打开 "Content-Disposition: inline; filename=文件名.mp3"
    response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
    // 告知浏览器文件的大小
    response.addHeader("Content-Length", String.valueOf(file.length()));
}

doDownLoadFile():ダウンロードファイル

public static ResultVo<String> doDownLoadFile(File file, HttpServletResponse response) {
    
    
    // 法一:IOUtils
    /*try (FileInputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {
        // 2.下载文件
        IOUtils.copy(in, out);
        log.info("【文件下载】文件下载成功");
        return null;
    } catch (FileNotFoundException e) {
        log.error("【文件下载】下载文件时,没有找到相应的文件,文件路径为{}", file.getAbsolutePath());
        return ResultVoUtil.error(e.getMessage());
    } catch (IOException e) {
        log.error("【文件下载】下载文件时,出现文件IO异常");
        return ResultVoUtil.error(e.getMessage());
    }*/

    // 法二:将文件以流的形式一次性读取到内存,通过响应输出流输出到前端
    /*try (InputStream in = new BufferedInputStream(new FileInputStream(file));
         OutputStream out = new BufferedOutputStream(response.getOutputStream())) {
        byte[] buffer = new byte[in.available()];
        in.read(buffer);
        out.write(buffer);
        log.info("【文件下载】文件下载成功");
        return null;
    } catch (IOException e) {
        log.error("【文件下载】下载文件时,出现文件IO异常");
        return ResultVoUtil.error(e.getMessage());
    }*/

    // 法三:将输入流中的数据循环写入到响应输出流中,而不是一次性读取到内存,通过响应输出流输出到前端
    try (InputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {
    
    
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = in.read(buffer)) != -1) {
    
    
            out.write(buffer, 0, len);
        }
        log.info("【文件下载】文件下载成功");
        return null;
    } catch (FileNotFoundException e){
    
    
        log.error("【文件下载】下载文件时,没有找到相应的文件,文件路径为{}", file.getAbsolutePath());
        return ResultVoUtil.error(e.getMessage());
    } catch (IOException e) {
    
    
        log.error("【文件下载】下载文件时,出现文件IO异常");
        return ResultVoUtil.error(e.getMessage());
    }
}

[注意]: ファイルをダウンロードした後は、値を再度返さないでください。返さないと、例外がスローされますjava.lang.IllegalStateException

java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
	at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:473) ~[tomcat-embed-core-8.5.28.jar:8.5.28]
	at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleHttpMediaTypeNotAcceptable(DefaultHandlerExceptionResolver.java:299) ~[spring-webmvc-5.0.4.RELEASE.jar:5.0.4.RELEASE]
	at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.doResolveException(DefaultHandlerExceptionResolver.java:180) ~[spring-webmvc-5.0.4.RELEASE.jar:5.0.4.RELEASE]
	at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:140) [spring-webmvc-5.0.4.RELEASE.jar:5.0.4.RELEASE]...

このようなことは絶対に書かないでください!

public static ResultVo<String> doDownLoadFile(File file, HttpServletResponse response) {
    
    
    try (InputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {
    
    
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = in.read(buffer)) != -1) {
    
    
            out.write(buffer, 0, len);
        }
        log.info("【文件下载】文件下载成功");
        
        // 千万不要这样写,否则会报错
        return ResultVoUtil.success("【下载成功】");
    } catch (FileNotFoundException e){
    
    
    	....    
    }
}

おすすめ

転載: blog.csdn.net/sco5282/article/details/121275436#comments_28358188
おすすめ