zoukankan      html  css  js  c++  java
  • [SpingBoot guides系列翻译]文件上传

    文件上传

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

    概况

    参考链接

    原文

    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运行了。

  • 相关阅读:
    测试某个方法的执行时间
    DataTable与DataGridView绑定
    《认知与设计——理解UI设计准则》笔记(7) 我们的注意力有限,记忆力也不完美
    常用软件收集
    通过反射得到某个实体的属性值
    获取某个字段的最大值
    C# ServerVariables参数说明
    C# 将网址转换为16进制可用网址,防盗链,%%
    不加修改权限的前提,要分辨出那些图片是删除不用的
    C# Winform 域名得到(查询)(服务器所在) IP  cs
  • 原文地址:https://www.cnblogs.com/sheldon-lou/p/10822597.html
Copyright © 2011-2022 走看看