首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[SpingBoot guides系列翻译]文件上传

[SpingBoot guides系列翻译]文件上传

作者头像
_淡定_
发布2019-05-15 10:51:41
10.1K0
发布2019-05-15 10:51:41
举报
文章被收录于专栏:dotnet & javadotnet & java

文件上传

这节的任务是做一个文件上传服务。

概况

参考链接

原文

thymeleaf

spring-mvc-flash-attributes

@ControllerAdvice

你构建的内容

分两部分,

  1. 服务端,由springboot构建。
  2. 客户端,是一个简单的html网页用来测试上传文件。
你需要的东西
  • 大约15min
  • 喜欢的编辑器或IDE(这里用IntelliJ)
  • jdk1.8+
  • Maven 3.2+ 或Gradle 4+
如何完成

跟着教程一步一步走。

通过Maven来构建

创建项目结构

mkdir -p src/main/java/hello,其实也就是在IntelliJ里面新建一个空的Java项目,然后添加一个main.java.hellopackage。

添加pom.xml文件。

<?xml version="1.0" encoding="utf-8" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-uploading-files</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.1.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.1.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.1.4.RELEASE</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

spring-boot-starter-thymeleaf 是java的服务端模板引擎。

Spring Boot Maven Plugin 有以下几个作用

  • 把项目打包成一个可执行的jar文件。
  • 搜索并标记public static void main()为可执行类。
  • 使用内置的依赖管理

之前设置的parent和dependency里面的version只指定了RELEASE,这里执行mvn compile的时候报了个错

[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[WARNING] 'parent.version' is either LATEST or RELEASE (both of them are being deprecated) @ line 13, column 18
[WARNING] 'dependencies.dependency.version' for org.springframework.boot:spring-boot-starter-thymeleaf:jar is either LATEST or RELEASE (both of them are being deprecated) @ line 28, column 22

大意就是parent.version的LATEST 和RELEASE的值设置都是已经被废弃,所以,我们这里需要指定一下具体的版本。2.1.4.RELEASE

这个pom.xml配置在后面会报个错,Re-run Spring Boot Configuration Annotation Processor to update generated metadata,需要添加一个dependency <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>

创建一个IStoreService

Controller 里面需要实现一些文件上传或者是读取的逻辑,我们可以在hello.storage包中创建一个IStorageService服务来处理这些。然后在controller中使用它。面向接口编程。

src/main/java/hello/storage/IStorageService.java

package hello.storage;

import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Path;
import java.util.stream.Stream;

public interface IStorageService {
    void init();

    void store(MultipartFile file);
    //
    Stream<Path> loadAll();

    Path load(String fileName);

    Resource loadAsResource(String filename);

    void deleteAll();
}
创建一个StorageProperties
package hello.storage;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("storage")
public class StorageProperties {

    /**
     * Folder location for storing files
     */
    private String location = "upload-dir";

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

}

主要用来配置上传相关的设置,比如文件夹路径。

创建FileSystemStorageService实现这个IStoreService接口

src/main/java/hello/storage/FileSystemStorageService.java

/*
 * Copyright (c) 2019.
 * lou
 */

package hello.storage;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;

@Service
public class FileSystemStorageService implements IStorageService {
    private final Path rootLocation;

    @Autowired
    public FileSystemStorageService(StorageProperties storageProperties) {
        this.rootLocation = Paths.get(storageProperties.getLocation());
    }

    @Override
    public void init() {
        System.out.println("初始化");
        try {
            Files.createDirectories(rootLocation);
        } catch (IOException e) {
            throw new StorageException("无法初始化", e);
        }
    }

    @Override
    public void store(MultipartFile file) {
        String fileName = StringUtils.cleanPath(file.getOriginalFilename());
        try {
            if (file.isEmpty()) {
                throw new StorageException("不能保存空文件" + fileName);
            }
            if (fileName.contains("..")) {
                //相对路径安全检查
                throw new StorageException("不能保存文件" + fileName + "到当前文件夹外");
            }
            try (InputStream inputStream = file.getInputStream()) {
                Files.copy(inputStream, this.rootLocation.resolve(fileName), StandardCopyOption.REPLACE_EXISTING);
            }
        } catch (IOException e) {
            new StorageException("保存文件失败:" + fileName, e);
        }

    }

    @Override
    public Stream<Path> loadAll() {
        System.out.println("获取所有");
        try {
            return Files.walk(this.rootLocation, 1)
                    .filter(path -> !path.equals(this.rootLocation))
//                    .map(path -> rootLocation.relativize(path));
                    //::表示一个委托
                    .map(this.rootLocation::relativize);
        } catch (IOException e) {
            throw new StorageException("读取保存的文件失败", e);
        }
    }

    @Override
    public Path load(String fileName) {
        System.out.println("加载单个文件" + fileName + "路径");
        return rootLocation.resolve(fileName);
    }

    @Override
    public Resource loadAsResource(String filename) {
        System.out.println("返回" + filename + "Resource类型的内容");
        try {
            Path file = load(filename);
            Resource resource = new UrlResource(file.toUri());
            if (resource.exists() || resource.isReadable()) {
                return resource;
            }
            throw new StorageFileNotFoundException("文件" + filename + "不存在");

        } catch (MalformedURLException e) {
            throw new StorageException("无法读取文件" + filename, e);
        }
    }

    @Override
    public void deleteAll() {
        System.out.println("删除所有");
        FileSystemUtils.deleteRecursively(rootLocation.toFile());

    }
}

这里需要把实现类加上@Service注解,。

定义一个StorageFileNotFound Exception

src/main/java/hello/storage/StorageFileNotFoundException.java

/*
 * Copyright (c) 2019.
 * lou
 */

package hello.storage;

public class StorageFileNotFoundException extends RuntimeException {
    public StorageFileNotFoundException(String message) {
        super(message);
    }

    public StorageFileNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}
创建一个文件上传controller

有了上面的IStorageService,下面就可以开始创建FileUploadController了。

src/main/java/hello/FileUploadController.java

/*
 * Copyright (c) 2019.
 * lou
 */

package hello;

import hello.storage.IStorageService;
import hello.storage.StorageFileNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.stream.Collectors;

@Controller
public class FileUploadController {
    private final IStorageService storageService;

    @Autowired
    public FileUploadController(IStorageService storageService) {
        this.storageService = storageService;
    }

    @GetMapping("/")
    public String listUploadedFiles(Model model) {
        model.addAttribute("files", storageService.loadAll().map(
                path -> MvcUriComponentsBuilder.fromMethodName(
                        FileUploadController.class,
                        "serveFile",
                        path.getFileName().toString())
                        .build().toString())
                .collect(Collectors.toList()));

        return "uploadForm";
    }

    @GetMapping("/files/{filename:.+}")
    @ResponseBody
    public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
        Resource file = storageService.loadAsResource(filename);

        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=\"" + file.getFilename() + "\"").body(file);
    }

    @PostMapping("/")
    public String handleFileUpload(MultipartFile file, RedirectAttributes redirectAttributes) {
        storageService.store(file);
        redirectAttributes.addFlashAttribute("message", "you successfuly uploaded " + file.getOriginalFilename() + "!");
        return "redirect:/";
    }

    @ExceptionHandler(StorageFileNotFoundException.class)
    public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
        return ResponseEntity.notFound().build();
    }

}

@Controller注解要加上。 构造函数上加上@AutoWired注解,Spring就会自动装配,因为构造函数上有IStorageService,Spring会去找实现这个类的@Service bean。然后定义几个方法,以及对应的路由。handleStorageFileNotFound方法用来处理当前controller出现的StorageFileNotFound异常。

  • GET /路由通过StorageService获取所有上传的文件列表,然后装载到Thymeleaf模板引擎中。通过MvcUriComponentsBuilder来计算得到实际的链接。
  • GET /files/{filename}加载资源,如果存在的话通过Content-Disposition头返回给浏览器用于下载。
  • POST /用于接收file,然后传递给storageService处理。
创建一个简单的HTML模板

src/main/resources/templates/uploadForm.html

<html xmlns:th="http://www.thymeleaf.org">
<body>

<div th:if="${message}">
    <h2 th:text="${message}"/>
</div>

<div>
    <form method="POST" enctype="multipart/form-data" action="/">
        <table>
            <tr><td>File to upload:</td><td><input type="file" name="file" /></td></tr>
            <tr><td></td><td><input type="submit" value="Upload" /></td></tr>
        </table>
    </form>
</div>

<div>
    <ul>
        <li th:each="file : ${files}">
            <a th:href="${file}" th:text="${file}" />
        </li>
    </ul>
</div>

</body>
</html>

有3点:

  • 第一个div中是可选的message参数,用来展示spring mvc设置的flash-scoped message
  • 第二个div用来给用户添加上传文件。
  • 第三个div显示所有的文件。
调节上传文件的相关限制

一般来说,我们会设置上传的文件大小。设想一下如果让spring去处理一个5G的文件上传。可以通过如下方法设置。

添加application.properties文件。

src/main/resources/application.properties

# Copyright (c) 2019.
# lou
#

spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB

设置了最大文件大小和最大的请求大小,这样如果上传的文件太大,会获取到异常。

Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Mon May 06 17:46:51 CST 2019 There was an unexpected error (type=Internal Server Error, status=500). Maximum upload size exceeded; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (727520) exceeds the configured maximum (131072)

max-request-size里面包括所有input的内容,也就是说request-size≥file-size。

定义一个FileUploadExceptionAdvice来处理MaxUploadSizeExceededException

这个org.springframework.web.multipart.MaxUploadSizeExceededException在是无法在控制器里面获取到的,所以可以通过@ControllerAdvice来处理。

src/main/java/hello/storage/FileUploadExceptionAdvice.java

/*
 * Copyright (c) 2019.
 * lou
 */

package hello.storage;

import org.apache.tomcat.util.http.fileupload.FileUploadBase;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@ControllerAdvice
public class FileUploadExceptionAdvice {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<ResponseResult> handleMaxSizeException(
            MaxUploadSizeExceededException ex,
            HttpServletRequest request,
            HttpServletResponse response) {
        long actual = request.getContentLengthLong();
        long permitted = -1L;
        if (ex.getCause() != null && ex.getCause().getCause() instanceof FileUploadBase.SizeLimitExceededException) {
            FileUploadBase.SizeLimitExceededException causeEx = (FileUploadBase.SizeLimitExceededException) ex.getCause().getCause();
            permitted = causeEx.getPermittedSize();
        }
        return ResponseEntity.ok(new ResponseResult("上传文件大小:"+actual + ",超过了最大:" + permitted, false));
    }
}

通过MaxUploadSizeExceededException.getCause()获取内部的SizeLimitExceededException异常详细信息,再通过ResponseEntity.ok()返回json数据。

构建可执行程序

下面就到了写Application.java 的时候了。

src/main/java/hello/Application.java

/*
 * Copyright (c) 2019.
 * lou
 */

package hello;

import hello.storage.IStorageService;
import hello.storage.StorageProperties;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableConfigurationProperties({StorageProperties.class})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    CommandLineRunner run(IStorageService storageService) {
        System.out.println("进入run方法");
        return args -> {
            storageService.deleteAll();
            storageService.init();
        };
    }
}

CommandLineRunner+@Bean确保程序启动的时候会运行。

@SpringBootApplication提供一下几点:

  • 表名这个configuration 类里面声明了一些@Bean方法。
  • 触发 auto-configuration
  • 开启 component 扫描.
  • 等于同时定义了 @Configuration, @EnableAutoConfiguration and @ComponentScan.

@EnableConfigurationProperties使得StorageProperties可以作为配置类。

运行输入 mvn spring-boot:run

图片
图片

打包输入mvn package。然后生成jar就可以用java -jar xxx.jar运行了。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-05-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文件上传
    • 概况
      • 参考链接
      • 你构建的内容
      • 你需要的东西
      • 如何完成
    • 通过Maven来构建
      • 创建项目结构
      • 创建一个IStoreService
      • 创建一个StorageProperties
      • 创建FileSystemStorageService实现这个IStoreService接口
      • 定义一个StorageFileNotFound Exception
      • 创建一个文件上传controller
      • 创建一个简单的HTML模板
      • 调节上传文件的相关限制
      • 定义一个FileUploadExceptionAdvice来处理MaxUploadSizeExceededException
    • 构建可执行程序
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档