Spring Boot上傳文件+部署到Tomcat

1 概述

Spring Boot上傳文件,根據官方uploadfile示例修改的,能夠打成war放到服務器上(筆者使用的是Tomcat).主要步驟是建立異常類,屬性類,接口類與控制器類,最後進行少許修改打包部署到服務器上.html

2 環境

  • win10
  • Tomcat 9.0.30
  • IDEA 2019.03
  • Spring boot 2.2.2 RELEASE

3 新建工程

選擇spring initializer:java

在這裏插入圖片描述

改一下包名,打包選項這裏能夠jar能夠war,選jar的話能夠在build的時候再生成war.git

在這裏插入圖片描述

這裏用的是模板引擎Thymeleaf,選擇spring web與Thymeleaf.github

在這裏插入圖片描述

在這裏插入圖片描述

最後點擊finish.web

4 新建包

4個包,service,properties,controller,exception.正則表達式

在這裏插入圖片描述

5 exception

處理兩個異常,分別是存儲異常與存儲文件找不到異常.spring

在這裏插入圖片描述

5.1 StorageException

package kr.test.exception;

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

    public StorageException(String message,Throwable cause)
    {
        super(message,cause);
    }
}

5.2 StorageFileNotFoundException

package kr.test.exception;

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

    public StorageFileNotFoundException(String message,Throwable cause)
    {
        super(message,cause);
    }
}
Exception(String message,Throwable cause);

這個構造函數中的cause是引發這個異常的異常,容許空值,若是是空值則表示這個引發這個異常的異常不存在或者未知.json

在這裏插入圖片描述

6 properties

新建StorageProperties.java,設定存儲文件的位置,就是location的值,可使用"../../"這樣的值,什麼也不加的話會在項目路徑下新建文件夾,如有同名的文件夾會被刪除再從新建立.瀏覽器

注意一下權限的問題,後面部署到Tomcat上面時可能會由於沒有寫權限而不能寫入文件,要確保文件夾擁有寫權限.tomcat

package kr.test.properties;

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

@ConfigurationProperties("storage")
public class StorageProperties {
    private String location = "upload_dir";
    public String getLocation()
    {
        return location;
    }
    
    public void setLocation(String location)
    {
        this.location = location;
    }
}

這裏使用@ConfigurationProperties會報紅,提示沒有@EnableConfigurationProperties:

在這裏插入圖片描述

能夠先無論,後面會在Main類中添加@EnableConfigurationProperties(StorageProperties.class).

7 service

先加一個StorageService接口:

package kr.test.service;

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

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

public interface StorageService
{
    void init();
    void store(MultipartFile file);
    Stream<Path> loadAll();
    Path load(String filename);
    Resource loadAsResource(String filename);
    void deleteAll();
}

而後新建一個FileSystemStorageService實現該接口:

package kr.test.service;

import kr.test.exception.StorageException;
import kr.test.exception.StorageFileNotFoundException;
import kr.test.properties.StorageProperties;
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 FileSystemStroageService implements StorageService
{
    private final Path rootLocation;

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

    @Override
    public void init()
    {
        try {
            Files.createDirectories(rootLocation);
        }
        catch (IOException e)
        {
            throw new StorageException("Could not initialize storage",e);
        }
    }

    @Override
    public void deleteAll()
    {
        FileSystemUtils.deleteRecursively(rootLocation.toFile());
    }

    @Override
    public Path load(String filename)
    {
        return rootLocation.resolve(filename);
    }

    @Override
    public Stream<Path> loadAll()
    {
        try
        {
            return Files.walk(rootLocation,1)
                    .filter(path -> !path.equals(rootLocation))
                    .map(rootLocation::relativize);
        }
        catch (IOException e)
        {
            throw new StorageException("Failed to read stored file.",e);
        }
    }

    @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 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(".."))
            {
                throw new StorageException("Cannot store file with relative path outside current directory"+filename);
            }
            try(InputStream inputStream = file.getInputStream())
            {
                Files.copy(inputStream,rootLocation.resolve(filename), StandardCopyOption.REPLACE_EXISTING);
            }
        }
        catch (IOException e)
        {
            throw new StorageException("Failed to store file : "+ filename,e);
        }
    }
}

7.1 init

@Override
public void init()
{
    try {
        Files.createDirectories(rootLocation);
    }
    catch (IOException e)
    {
        throw new StorageException("Could not initialize storage",e);
    }
}

使用java.nio.file.Files.createDirectories()建立存儲目錄,能夠創建多級目錄.

7.2 deleteAll

@Override
public void deleteAll()
{
    FileSystemUtils.deleteRecursively(rootLocation.toFile());
}

使用工具類FileSystemUtils的方法遞歸刪除文件與文件夾.參數是一個File. 下面是方法源碼:

public static boolean deleteRecursively(File root) 
{
    if (root != null && root.exists()) 
    {
        if (root.isDirectory()) 
        {
            File[] children = root.listFiles();
            if (children != null) 
            {
                for (File child : children) 
                {
                    deleteRecursively(child);
                }
            }
        }
        return root.delete();
    }
    return false;
}

首先判斷根是否爲空,不爲空的話判斷是不是目錄,不是目錄的話直接刪除,是目錄的話,利用listFiles()獲取全部文件及文件夾,判斷是否爲空並進行遞歸刪除.

7.3 load

@Override
public Path load(String filename) {
	return rootLocation.resolve(filename);
}

Path.resolve(String)返回相對於this的路徑,具體來講,等於執行

cd rootLocation
cd filename
pwd

返回pwd的值.

7.4 loadAll

@Override
public Stream<Path> loadAll()
{
    try 
    {
        return Files.walk(rootLocation,1)
                .filter(path -> !path.equals(rootLocation))
                .map(rootLocation::relativize);
    }
    catch (IOException e)
    {
        throw new StorageException("Failed to read stored file.",e);
    }
}

Files.walk遍歷目錄,返回一個Stream<Path>,返回的Stream包含打開的一個或多個目錄的引用,會在Stream關閉時關閉,第二個參數1表示遍歷的最大深度.

而後對這個Stream進行filter過濾,這裏是把與rootLocation不相等的Path留下,注意是不相等,就是留下filter()中條件爲真的Path,不是把條件爲真的Path給"刪去".

最後進行map,relativize返回參數相對於調用者的路徑,這裏是返回Stream中的每一個Path相對於rootLocation的路徑. 對於relativize,不管什麼狀況下:

Path a = xxxx;
Path b = xxxx;

都有

a.relativize(a.resolve(b)).equals(b)

爲真.

7.5 loadAsResource

@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);
    }
}

這裏的Resource是org.springframework.core.io.Resource,是一個接口,能夠經過它訪問各類資源,實現類有UrlResource,InputStreamResource等,這裏利用Path.toUri()把file轉換爲Resource後,判斷這個源是否存在或者是否可讀並返回,不然拋出存儲文件找不到異常.

7.6 store

@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(".."))
        {
            throw new StorageException("Cannot store file with relative path outside current directory"+filename);
        }
        try(InputStream inputStream = file.getInputStream()) 
        {
            Files.copy(inputStream,rootLocation.resolve(filename), StandardCopyOption.REPLACE_EXISTING);
        }
    }
    catch (IOException e)
    {
        throw new StorageException("Failed to store file : "+ filename,e);
    }

getOriginalFilename()獲取文件原名字,而後經過StringUtils.cleanPath()將其標準化,.處理掉"."與"..",而後判斷文件是否爲空與是否包含相對路徑,沒有的話利用Files.copy()進行復制,resolve獲取filename相對於rootLocation的值,複製選項是REPLACE_EXISTING. StandardCopyOption有三個可選值:

  • ATOMIC_MOVE:原子性的移動操做,通常在移動文件或目錄時使用.
  • COPY_ATTRIBUTES:複製屬性,能夠保留源文件或源目錄的屬性.
  • REPLACE_EXISTING:替換已存在的文件.

8 controller

新建FileUploadController.

在這裏插入圖片描述

package kr.test.controller;

import kr.test.exception.StorageFileNotFoundException;
import kr.test.service.StorageService;
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 StorageService storageService;

    @Autowired
    public FileUploadController(StorageService 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(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes)
    {
        storageService.store(file);
        redirectAttributes.addFlashAttribute("message","You successully uploaded "+file.getOriginalFilename()+"!");
        return "redirect:/";
    }

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

8.1 listUploadedFiles

@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是@RequestMapping(method = RequestMethod.GET)的簡化寫法,將HTTP GET路徑映射到特定的處理方法上. 方法的參數是spring MVC中的Model,Model實質上是一個Map,添加的key能夠在視圖中用${key}獲取值,好比,這裏添加了"files"做爲key,則在視圖中可用 ${files}獲取值.

MvcUriComponentsBuilder能夠爲Controller指定uri,fromMethod簡單地說就是會調用FileUploadController的serveFile(),參數是path.getFileName().toString(),因爲serveFile()返回的是Stream<Path>,利用Stream的collect將其轉換成List添加到model中,而後返回uploadForm,表示這是視圖的名稱,會到resource/templates下尋找.

這裏說一下RequestMapping與Model:

8.1.1 RequestMapping

能夠用@RequestMapping()來映射URL,能夠映射到某個類或某個具體方法.@RequestMapping經常使用的有如下屬性:

  • value:請求的URL路徑,支持URL模板,正則表達式.
  • method:HTTP請求方法,如GET,POST,PUT,DELTE等.
  • consumes:容許的媒體類型,如consumes="application/json".對應於HTTP請求的Content-Type.
  • produces:相應的媒體類型,如produces="application/json",對於HTTP請求的Accept.
  • params:請求參數,如params="action=update".
  • headers:請求頭.

Spring提供了簡化的@RequestMapping,提供了新的註解來標識HTTP方法:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • ...

因此這裏的@GetMapping是簡化了的@RequestMapping.

8.1.2 Model

能夠向Model添加視圖所須要的變量,Model主要有如下方法:

Model addAttribute(Object value);
Model addAttribute(String name,Object value);
Model addAllAttributes(Map attributes);
Model addAllAttributes(Collection<?> attributes);
Model mergeAttributes(Map attributes);
boolean containAttribute(String name);

addAttribute()添加一個變量,對於兩個參數的,使用name做爲變量名稱,後面的是值,對於只有一個Object的,變量的名字就是類名字首字母小寫後轉爲的java變量. addAttributes()添加多個變量,若是變量存在則覆蓋,其中參數爲Collection<?>的方法添加變量名時與addAttribute(Object)的命名規範相似. mergeAttributes()也是添加多個變量,不過變量已存在的話會忽略. containAttributte()判斷是否存在變量.

8.2 serveFile

@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);
}

這裏的@GetMapping用來表示顯示的用來供下載的文件名,@ResponseBody表示直接返回內容而不是視圖名,由於默認返回的是視圖名稱,@ResponseBody對於String直接返回,不然默認使用Jackson進行序列化.

@PathVariable表示這是@GetMapping中的參數的值,能夠省略,默認同名,就是形參的名字與GetMapping中的名字同樣,從中取值賦給形參,經過filename加載資源後,做爲ResponseEntity的請求體. ResponseEntity從HttpEntity繼承而來,ResponseEntity.ok()是一個靜態方法,表示構建一個狀態爲"ok"的ResponseEntity,而後添加請求頭.

HttpHeaders.CONTENT_DISPOSITION,"attachment;filename=\""+file.getFilename()+"\""

content_disposition表示文件是直接在瀏覽器打開仍是下載,attachment表示是要下載,文件名爲file.getFilename().

8.3 handleFileUpload

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

@PostMapping()與@GetMapping()相似,只不過方法不是GET而是POST.@RequestParam表示請求參數,裏面的是請求參數的名字,使用MultipartFile來處理文件上傳. RedirectAttributes是用於重定向使用的,能夠附帶參數,RedirectAttributes有兩種帶參的形式:

addAttribute(String name,Object value);
addFlashAttribute(String name,Object value);

addAttribute()至關於直接在重定向的地址添加

name=value

這樣的形式,會將參數暴露在重定向的地址上.

而addFlashAttribute()隱藏了參數,只能在重定向的頁面中獲取參數的值,用到了session,session跳轉到頁面後就會刪除對象. handleFileUpload首先保存文件,而後添加一個保存成功的信息,因爲Controller中重定向能夠返回以"redirect:"或以"forward:"爲前綴的URI,所以返回"redirect:/",重定向到根.

8.4 handleStorageFileNotFound

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

@ExceptionHandler()註解會處理Controller層拋出的全部StorageFileNotFoundException類及其子類的異常,ResponseEntity.notFound()至關於返回404標識碼.

9 main

package kr.test;

import kr.test.properties.StorageProperties;
import kr.test.service.StorageService;
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 TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

    @Bean
    CommandLineRunner init(StorageService storageService)
    {
        return (args) ->
        {
            storageService.deleteAll();
            storageService.init();
        };
    }
}

在原來的基礎上添加

@EnableConfigurationProperties(StorageProperties.class)

@Bean
CommandLineRunner init(StorageService storageService)
{
    return (args) ->
    {
        storageService.deleteAll();
        storageService.init();
    };
}

@EnableConfigurationProperties能夠爲帶有@ConfigurationProperties註解的Bean提供有效的支持,將帶有@Configuration註解的類注入爲Spring的Bean,在這裏是使StorageProperties的@ConfigurationProperties生效,若是沒有這一行會報紅:

在這裏插入圖片描述

@Bean標註在方法上,等價於spring的xml配置文件的<bean>,註冊bean對象. CommandLineRunner接口用於應用初始化後去執行一段代碼邏輯,這段代碼在整個應用週期只執行一次.

10 application.properties

這裏能夠設置一些環境配置屬性,Spring Boot容許準備多個配置文件,在部署時能夠指定那個配置文件覆蓋默認的application.properties.這裏是有關上傳文件的設置:

在這裏插入圖片描述

默認以下:

spring.servlet.multipart.enabled=true 
spring.servlet.multipart.file-size-threshold=0
spring.servlet.multipart.location=
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.resolve-lazily=false

enabled表示容許上傳,file-size-threshold表示上傳文件超過必定長度就先寫入臨時文件,單位MB或KB,location是臨時文件存放目錄,不設定的話使用web服務器提供的臨時目錄.max-file-size表示單個文件最大長度,默認1MB,max-request-size爲單次HTTP請求上傳的最大長度,默認10MB,resolve-lazily表示文件和參數被訪問的時候再解析成文件.

在這裏只需把max-size調大一點便可.

在這裏插入圖片描述

11 測試

這是在本地進行的測試.直接在IDE上點擊運行應用,而後打開瀏覽器輸入:

localhost:8080

在這裏插入圖片描述

12 打包部署到Tomcat上

Spring Boot一般打成jar包或war包,這裏部署到Tomcat上的是打成war包.

12.1 改變打包方式

pom.xml中,<packaing>改爲war:

在這裏插入圖片描述

12.2 去除Tomcat依賴

Spring Boot默認自帶了一個嵌入式的Tomcat,須要把Tomcat依賴方式改成provided. pom.xml中,在<dependencies>添加:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

12.3 修改Main類

修改Main類,讓其繼承SpringBootServletInitializer,重載configure(),同時main()保持不變.

@SpringBootApplication
public class MainClass extends SpringBootServletInitializer
{
	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
	{
		return application.sources(MainClass.class);
	}
	//main()不變
}

12.4 路徑問題

這個很重要,設置不當的話就沒法訪問了,主要就是四個路徑:

  • action:

在這裏插入圖片描述

  • @GetMapping

在這裏插入圖片描述

  • @PostMapping

在這裏插入圖片描述

  • redirect

在這裏插入圖片描述

12.4.1 action

這個是絕對路徑,要加上/war項目名.

/war項目名/上傳路徑名

在這裏插入圖片描述

好比這裏war項目名是kr,上傳路徑名是upload.

12.4.2 @GetMapping

這個是相對路徑,相對於當前項目的路徑,不用加上/war項目名.

/上傳路徑名

在這裏插入圖片描述

這裏是upload.

12.4.3 @PostMapping

與@GetMapping同樣,上傳路徑名.

/上傳路徑名

在這裏插入圖片描述

12.4.4 redirect

這個是返回的重定向的路徑名,相對路徑,與上兩個同樣,也是上傳路徑名.

/上傳路徑名

在這裏插入圖片描述

12.5 設置打包名字

在<build>中添加<finalName>,指定打包出來的war名,注意這個要與上面的war項目名同樣,這裏設置的是kr.

在這裏插入圖片描述

12.6 Maven打包

運行

mvn package

便可打包,對於IDEA,能夠在IDEA右側欄的Maven中,打開Lifecycle,選擇package:

在這裏插入圖片描述

12.7 打包完成

打包後的war默認放在target下,名字默認爲<artifactId>+<version>.

在這裏插入圖片描述

在這裏插入圖片描述

12.8 上傳到服務器

上傳的話筆者用的是密鑰認證的scp:

scp -i xxxx\id_rsa kr.war username@ip:/usr/local/tomcat/webapps

放到服務器的Tomcat下的webapps目錄.

12.9 開啓Tomcat

進入到Tomcat目錄的bin下:

cd /usr/local/tomcat/bin
./startup.sh

若是正在運行的話就不用啓動了,由於會自動檢測到webapps目錄的變化,把新的war自動解包.

12.10 測試

略,與本地測試相似,不過要注意的是上傳的文件夾是在tomcat/bin下,想要修改的話能夠修改StorageProperties的location.

13 源碼

github

碼雲

14 參考

1.ConfigurationProperties

2.CommandLineRunner

3.RedirectAttribute

相關文章
相關標籤/搜索