ファイルのアップロードとダウンロードは実際のプロジェクトでもよく使われるので、今日はここでまとめておきます。
このブログは、ファイルのアップロード/ダウンロード機能を完了することだけを目的としているわけではありません。さらに重要なのは、ログ記録、入力/戻りパラメーターの検証、単一メソッドの責任などのコーディングを標準化することです。
開発環境:
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;
}
}
例証します:
- ロギングを行う
- パラメータの確認
- カスタム例外
ParamErrorException
処理 - 戻り結果の統合カプセル化
ResultVo
- 単一責任。ツール クラスを使用してファイル アップロード操作を実行します。
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);
}
}
例証します:
- 文字列ツール クラスをカプセル化して、
StringUtil
文字列が空かどうかを判断します。 - ファイルのアップロード パスを取得します。
String uploadPath = fileConfig.getUploadPath();
このコードは非常に興味深いです。 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;
}
例証します:
- アップロードされたファイルにサフィックス (「.」で区別) がない場合は、アンダースコア「_」を直接連結し、ファイル名の重複を避けてからタイムスタンプを連結します。
- アップロードされたファイルにサフィックスが付いている場合は、ファイル名の「.」をアンダースコア「_」に変換し、タイムスタンプを結合します。
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){
....
}
}