【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:\Workflow\Eclipse WS\Restful-Web-Service-1\target\classes\com\example\kane\service\StorageProperties.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:\Workflow\Eclipse WS\Restful-Web-Service-1\target\classes\com\example\kane\service\FileSystemStorageService.class]
	- twostorageservice: defined in file [C:\Workflow\Eclipse WS\Restful-Web-Service-1\target\classes\com\example\kane\service\twostorageservice.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上傳文件例子的內容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM