zoukankan      html  css  js  c++  java
  • 【Spring Boot】关于上传文件例子的剖析

    Spring Boot 上传文件

    文件上传是一个基本需求,话不多说,我们直接演练

    功能实现

    增加ControllerFileUploadController

    代码

    package com.example.kane.Controller;
    import org.springframework.stereotype.Controller;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.web.bind.annotation.*;
    
    import java.io.IOException;
    import java.util.stream.Collectors;
    
    import org.springframework.core.io.Resource;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.multipart.MultipartFile;
    import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
    import org.springframework.web.servlet.mvc.support.RedirectAttributes;
    
    import com.example.kane.service.StorageException;
    import com.example.kane.service.StorageFileNotFoundException;
    import com.example.kane.service.StorageService;
    
    import com.example.kane.Controller.FileUploadController;
    @Controller
    public class FileUploadController {
        @Autowired()
        //@Qualifier("FileSystemStorageService") 
        private  StorageService storageService;
    
        @Autowired
        public FileUploadController(StorageService storageService) {
            this.storageService = storageService;
            System.out.println(this.storageService);
        }
        @GetMapping("/")
        public String listUploadedFiles(Model model) throws IOException {
        	System.out.println(this.storageService);
            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(@RequestParam("file") MultipartFile file,
                RedirectAttributes redirectAttributes) {
    
            storageService.store(file);
            redirectAttributes.addFlashAttribute("message",
                    "You successfully uploaded " + file.getOriginalFilename() + "!");
    
            return "redirect:/";
        }
    
        @ExceptionHandler(StorageFileNotFoundException.class)
        public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
        	System.out.println(11);
            return ResponseEntity.notFound().build();
        }
    }
    

    逻辑分析

    • 控制类汇总有一个私有的StorageService的类,做逻辑处理。
    • 发送GET 请求,URL 匹配到/时,进入的文件上传的页面,页面汇总包含已上传文件列表、上传按钮。此处使用了Thymeleaf模板引擎,后面会介绍
    • 发送GET请求,URL匹配到/files/{filename}时进行下载文件功能。
    • 发送POST请求,URL匹配到/时,进行上传文件的请求
    • 当遇到 StorageFileNotFoundException的时候异常处理

    增加ServiceStorageService

    代码

    package com.example.kane.service;
    
    import org.springframework.core.io.Resource;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    import org.springframework.context.annotation.Bean;
    import java.nio.file.Path;
    import java.util.stream.Stream;
    @Service
    public interface StorageService {
    
        void init();
    
        void store(MultipartFile file);
        @Bean
        Stream<Path> loadAll();
    
        Path load(String filename);
    
        Resource loadAsResource(String filename);
    
        void deleteAll();
    
    }
    

    逻辑分析

    上面的Service,只是一个接口,本例中,官方是面向接口编程,实现了JAVA的多态。后面会有介绍。

    增加一个Thymeleaf页面

    注:Thymeleaf后面整体介绍,此处简单的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>
    

    修改一些简单的配置application.properties

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

    修改Spring Boot Application类

    代码

    package com.example.kane;
    
    import org.springframework.beans.factory.annotation.Autowired;
    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.boot.web.client.RestTemplateBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    import org.slf4j.Logger;
    
    import com.example.kane.config.db_config;
    import com.example.kane.service.StorageService;
    
    import com.example.kane.service.StorageProperties;
    
    import org.slf4j.LoggerFactory;
    import org.springframework.web.client.RestTemplate;
    
    import com.example.kane.Model.Customer;
    
    @SpringBootApplication
    @EnableConfigurationProperties(StorageProperties.class)
    //@EnableScheduling
    public class RestfulWebService1Application{
    	
    	private static final Logger log = LoggerFactory.getLogger(RestfulWebService1Application.class);
    
        public static void main(String args[]) {
            SpringApplication.run(RestfulWebService1Application.class, args);
        }
        @Bean
        CommandLineRunner init(StorageService storageService) {
            return (args) -> {
                //storageService.deleteAll(); 
                storageService.init();
            };
        }
    }
    

    逻辑分析

    • 开启Spring Boot项目
    • 定义了一个项目启动后需要运行删除所有文件的逻辑。CommandLineRunner之前有做介绍。

    官网没有说明其他的Service类的定义

    按照官网至此已经创建完成了上传文件的应用,但是少了一部分内容,就是其他的Service的定义情况。下面做补充。

    接口StorageService的实现类FileSystemStorageService

    package com.example.kane.service;
    
    import java.util.stream.Stream;
    
    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;
    
    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 org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Primary;
    @Service(value="FileSystemStorageService")
    @Primary()
    public class FileSystemStorageService  implements StorageService {
        private final Path rootLocation;
    
        @Autowired
        public FileSystemStorageService(StorageProperties properties) {
        	System.out.println(properties.test);
            this.rootLocation = Paths.get(properties.getLocation());
        }
    
        @Override
        public void store(MultipartFile file) {
            String filename = StringUtils.cleanPath(file.getOriginalFilename());
            try {
                if (file.isEmpty()) {
                    throw new StorageException("Failed to store empty file " + filename);
                }
                if (filename.contains("..")) {
                    // This is a security check
                    throw new StorageException(
                            "Cannot store file with relative path outside current directory "
                                    + filename);
                }
                try (InputStream inputStream = file.getInputStream()) {
                    Files.copy(inputStream, this.rootLocation.resolve(filename),
                        StandardCopyOption.REPLACE_EXISTING);
                }
            }
            catch (IOException e) {
                throw new StorageException("Failed to store file " + filename, e);
            }
        }
    
        @Override
        public Stream<Path> loadAll() {
            try {
                return Files.walk(this.rootLocation, 1)
                    .filter(path -> !path.equals(this.rootLocation))
                    .map(this.rootLocation::relativize);
            }
            catch (IOException e) {
                throw new StorageException("Failed to read stored files", e);
            }
    
        }
    
        @Override
        public Path load(String filename) {
            return rootLocation.resolve(filename);
        }
    
        @Override
        public Resource loadAsResource(String filename) {
            try {
                Path file = load(filename);
                Resource resource = new UrlResource(file.toUri());
                if (resource.exists() || resource.isReadable()) {
                    return resource;
                }
                else {
                    throw new StorageFileNotFoundException(
                            "Could not read file: " + filename);
    
                }
            }
            catch (MalformedURLException e) {
                throw new StorageFileNotFoundException("Could not read file: " + filename, e);
            }
        }
    
        @Override
        public void deleteAll() {
            FileSystemUtils.deleteRecursively(rootLocation.toFile());
        }
    
        @Override
        public void init() {
            try {
                Files.createDirectories(rootLocation);
            }
            catch (IOException e) {
                throw new StorageException("Could not initialize storage", e);
            }
        }
    }
    

    属性类StorageProperties

    package com.example.kane.service;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties("storage")
    public class StorageProperties {
        /**
         * Folder location for storing files
         */
        private String location = "upload-dir";
    	public String test;
    
        public String getLocation() {
            return location;
        }
    
        public void setLocation(String location) {
            this.location = location;
        }
        public void settest(String test) {
        	this.test=test;
        }
        public String gettest() {
        	return test;
        }
    
    }
    

    异常类StorageFileNotFoundException StorageException定义

    //StorageFileNotFoundException
    package com.example.kane.service;
    
    public class StorageFileNotFoundException  extends StorageException{
        public StorageFileNotFoundException(String message) {
            super(message);
        }
    
        public StorageFileNotFoundException(String message, Throwable cause) {
            super(message, cause);
        }
    }
    //StorageException
    package com.example.kane.service;
    
    public class StorageException extends RuntimeException{
        public StorageException(String message) {
            super(message);
        	System.out.println(111111111);
        }
    
        public StorageException(String message, Throwable cause) {
            super(message, cause);
        	System.out.println(111111111);
    
        }
    }
    

    至此,运行项目。可以再上传与下载文件

    注:在StorageProperties中定义了文件在服务中的位置upload-dir

    介绍@ConfigurationProperties的用法

    上面例子的 @ConfigurationProperties

    @ConfigurationProperties可以使用application.properties中的属性。在官网的例子中,application.properties可以任意定义storage.test=123然后在类StorageProperties中书写get、set方法之后,就可以使用了。官网的例子并没有使用,我们可以将location的默认赋值改成如下做法

    • application.properties
    storage.location= upload-dir #只需要加这一行就可以
    
    • Storage.Properties
    @ConfigurationProperties("storage")
    public class StorageProperties {
        private String location;
        public String getLocation() {
            return location;
        }
    
        public void setLocation(String location) {
            this.location = location;
        }
    }
    

    按照如上操作,定义的@ConfigurationProperties("storage")才是有意义的,否则根本没有使用到。

    扩展:自定义一个Properties供自己使用

    默认情况下Spring Boot 会使用 Application.properties,我们再同级目录下创建文件storage.properties

    • storage.properties
    storage.location=upload-dir #注意这里要不加任何引号
    
    • Storage.Properties
    package com.example.kane.service;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.stereotype.Component;
    @Primary //标注当前类为主要的bean类
    @Configuration//让当前类能够被Spring识别
    @ConfigurationProperties(prefix="storage")
    @PropertySource("classpath:storage.properties") //配置路径
    public class StorageProperties {
        /**
         * Folder location for storing files
         */
        //private String location = "upload-dir";
    	private String location;
    
        public String getLocation() {
            return location;
        }
    
        public void setLocation(String location) {
            this.location = location;
        }
    }
    

    没解决的一个问题

    1. @Primary这个注解是必须的。

    我在这里遇到这个问题,报错信息如下,可以看到是spring没法识别使用哪个bean了,这块没弄明白,应为第一个properties是打包后的target目录下的,第二则是实际代码中的,但是确有冲突,我Primary加上之后,会让spring拿当前类为首要的当做configuration的类。如果是别的原因造成的望指正

    Parameter 0 of constructor in com.example.kane.service.FileSystemStorageService required a single bean, but 2 were found:
    	- storageProperties: defined in file [C:WorkflowEclipse WSRestful-Web-Service-1	argetclassescomexamplekaneserviceStorageProperties.class]
    	- storage-com.example.kane.service.StorageProperties: defined in null
    

    介绍面向本例中的面向接口编程实现的Java的多态

    在官网的例子中,在FileUploadController中定义了私有变量是,接口StorageService,而由于当前项目中只有一个类FileSystemStorageService实现了这个接口,所以项目能够正常运行。而加入我们项目中存在第二个类去实现StorageService会怎么样呢。

    创建第二个实现StorageService的类之后的错误

    • 创建Service类twostorageservice,不需要做什么具体实现。
    package com.example.kane.service;
    
    import java.nio.file.Path;
    import java.util.stream.Stream;
    import org.springframework.stereotype.Service;
    
    import org.springframework.core.io.Resource;
    import org.springframework.web.multipart.MultipartFile;
    @Service(value="twostorageservice")
    public class twostorageservice  implements StorageService {
    
    	@Override
    	public void init() {
    		// TODO Auto-generated method stub
    		
    	}
    
    	@Override
    	public void store(MultipartFile file) {
    		// TODO Auto-generated method stub
    		
    	}
    
    	@Override
    	public Stream<Path> loadAll() {
    		// TODO Auto-generated method stub
    		return null;
    	}
    
    	@Override
    	public Path load(String filename) {
    		// TODO Auto-generated method stub
    		return null;
    	}
    
    	@Override
    	public Resource loadAsResource(String filename) {
    		// TODO Auto-generated method stub
    		return null;
    	}
    
    	@Override
    	public void deleteAll() {
    		// TODO Auto-generated method stub
    		
    	}
    
    }
    
    • 结果报错如下
    Parameter 0 of constructor in com.example.kane.Controller.FileUploadController required a single bean, but 2 were found:
    	- FileSystemStorageService: defined in file [C:WorkflowEclipse WSRestful-Web-Service-1	argetclassescomexamplekaneserviceFileSystemStorageService.class]
    	- twostorageservice: defined in file [C:WorkflowEclipse WSRestful-Web-Service-1	argetclassescomexamplekaneservice	wostorageservice.class]
    	Action:
    
    Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
    

    信息很明显,出现了两个bean Spring不知道选择哪一个了。

    解决办法

    根据上面的提示,我们去解决它。

    1. 在冲突的两个Service中的某一个上,增加注解 @Primary
    //在主要的实现类 FileSystemStorageService 前面加@Primary
    @Primary()
    public class FileSystemStorageService  implements StorageService {
    }
    
    1. 对每个Service定义一个name,在使用时进行选择
    //FileSystemStorageService
    @Service(value="FileSystemStorageService")
    @Primary
    public class FileSystemStorageService  implements StorageService {}
    //twostorageservice
    @Service(value="twostorageservice")
    public class twostorageservice  implements StorageService {}
    // --------------------------使用的位置
    @Autowired()
    @Qualifier("twostorageservice") 
    private  StorageService storageService;
    // ------------------------controller中 我们打印一下Service,不管功能实现
     @GetMapping("/")
        public String listUploadedFiles(Model model) throws IOException {
        	System.out.println(this.storageService);
        }
    // 输出结果
    com.example.kane.service.twostorageservice@1364c
    

    注:实现时发现一个问题,@Primary 与@Qualifier不是两个并行的解决办法。方法2中也需要指定一个Service为主要实现类,否则还是会报错。

    至此,可以在代码运行时,动态的指定实现类。

    关于自定义异常处理

    @Exceptionhandler

    @Exceptionhandler在Controller中定义,对不同的Exception定义不同的处理方法。官网的例子中对StorageFileNotFoundException定义了处理方法。

    • 我们删除一个文件,然后点击其连接下载

    查看控制台输出

    2019-03-18 15:56:43.071  WARN 16004 --- [nio-8080-exec-2] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [com.example.kane.service.StorageFileNotFoundException: Could not read file: Data Analize.xls]
    

    查看页面输出

    找不到 localhost 的网页 找不到与以下网址对应的网页:http://localhost:8080/files/Data%20Analize.xls
    HTTP ERROR 404
    
    • 我们将Controller中@Exceptionhandler方法注释掉,在看控制台输出

    是一大串很长的Exception

    com.example.kane.service.StorageFileNotFoundException: Could not read file: Data Analize.xls
    	at com.example.kane.service.FileSystemStorageService.loadAsResource(FileSystemStorageService.java:83) ~[classes/:na]
    	at com.example.kane.Controller.FileUploadController.serveFile(FileUploadController.java:55) ~[classes/:na]
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_172]
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_172]
    	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_172]
    	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_172]
    	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:189) ~[spring-web-5.1.5.RELEASE.jar:5.1.5.RELEASE]
    
    • 我们再Controller中增加对异常类StorageException的处理方法
        @ExceptionHandler(StorageException.class)
        public ResponseEntity<?> storageException(StorageException exc) {
        	return new ResponseEntity<Object>("test",HttpStatus.GATEWAY_TIMEOUT);
        }
    //对异常页面做到自定义
    
    • 上传一个空文件,出发StorageException异常

    查看控制台输出

    2019-03-18 16:04:15.591  WARN 16004 --- [nio-8080-exec-1] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [com.example.kane.service.StorageException: Failed to store empty file New Text Document.txt]
    

    查看页面输出

    test
    

    ErrorController

    我们还可以定一个一个类实现ErrorController来对Controller的异常进行处理。

    关于模板引擎 Thymeleaf的用法

    POM.xml增加依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    

    修改默认模板路径

    默认的路径是resources/templates,我们可以再application.properties文件中配置如下属性spring.thymeleaf.prefix= classpath:/templates/test/进行修改

    使用

        @GetMapping("/")
        public String listUploadedFiles(Model model) throws IOException {
            //往模板文件中增加变量
            model.addAttribute("files", storageService.loadAll().map(
                    path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
                            "serveFile", path.getFileName().toString()).build().toString())
                    .collect(Collectors.toList()));
    
            return "uploadForm"; //模板文件的名字不带html
        }
    

    以上是总结Spring Boot上传文件例子的内容

  • 相关阅读:
    隆重推出PowerDesinger 12.1.0.1913破解补丁
    Glowcode 6.0 破解手记,.Net IL技术
    爬虫如何抓取到Asp.Net中__doPostBack获取新页面的数据
    谁是股市上的最大受益者
    带强名的.net程序反向工程后汇编中的问题总结
    让所有的实体店成为网店的体验店
    推出网站大鱼搜索
    利用搜索引擎技术抢注域名
    在程序中显示PDF文件不依赖于Arcobat Reader的PDF阅读器开发
    用开源Carrot2的后缀树算法做Web文本聚类
  • 原文地址:https://www.cnblogs.com/primadonna/p/10552907.html
Copyright © 2011-2022 走看看