SpringBoot2FileUpload(SpringBoot2官方文件上传下载DEMO)

前言

FileUpload文件上传是开发中经常遇到的事,通常都是网上copy一段代码来上传,可是你的代码足够完善吗,可以应对日益增长的文件需求吗,可以同时当上传和下载服务器吗,今天让我们来跟着Spring官方的Uploading Files教程进行优化和改造文件上传服务器(适应于少量文件上传,量大请使用DFS)。

2020年3月9日更新:

  • 新增处理中文文件名的方法,例如1583765828605巴厘岛.xlsx会变1583765828605魳_xlsx的未知格式问题。在改方法直接返回一个URLEncoder UTF-8编码的文件名即可。
	@GetMapping("/files/{filename:.+}")
	@ResponseBody
	public ResponseEntity<Resource> serveFile(@PathVariable String filename) throws UnsupportedEncodingException {
		//加载文件
		Resource file = storageService.loadAsResource(filename);
		log.info("download file:"+file.getFilename());
		//attachment附件下载模式,直接下载文件
		return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
				"attachment; filename=\"" + URLEncoder.encode(file.getFilename(), "UTF-8") + "\"").body(file);
	}

项目结构

核心代码如下:

  • application.yml
  • StorageController
  • StorageService&StorageServiceImpl
  • StorageExcetpion&StorageFileNotFoundExcetpion
  • HTML
    SpringBoot2FileUpload(SpringBoot2官方文件上传下载DEMO)_第1张图片
    Application.yml

server:
  port: 9999
  servlet:
      context-path: /fileupload
tomcat:
    remote-ip-header: x-forward-for
    uri-encoding: UTF-8
    max-threads: 10
    background-processor-delay: 30
spring:
    http:
      encoding:
        force: true
        charset: UTF-8
    application:
        name: spring-cloud-study-fileupload
    freemarker:
        request-context-attribute: request
        #prefix: /templates/
        suffix: .html
        content-type: text/html
        enabled: true
        cache: false
        charset: UTF-8
        allow-request-override: false
        expose-request-attributes: true
        expose-session-attributes: true
        expose-spring-macro-helpers: true
        #template-loader-path: classpath:/templates/
    servlet:
      multipart:
        max-file-size: 10MB
        max-request-size: 10MB
    file:
      devPath: C:\workspace\Temp\
      prodPath: /dev/fileupload/

Exception

  • StorageException
public class StorageException extends RuntimeException {

	private static final long serialVersionUID = 1L;

	public StorageException(String message) {
        super(message);
    }

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

  • StorageFileNotFoundException
public class StorageFileNotFoundException extends StorageException {

	private static final long serialVersionUID = 1L;

	public StorageFileNotFoundException(String message) {
        super(message);
    }

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

Service

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

  • Implement 实现

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.Value;
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 com.softdev.system.demo.entity.StorageException;
import com.softdev.system.demo.entity.StorageFileNotFoundException;
/**
 * FileUpload Service
 * @author zhengkai.blog.csdn.net
 * */
@Service
public class StorageServiceImpl implements StorageService {
	//从application.yml中读取
	@Value("${spring.file.devPath}")
	private String devPath;
	//从application.yml中读取
	@Value("${spring.file.prodPath}")
	private String prodPath;
	//请勿直接使用path,应该用getPath()
	private Path path;

	@Override
	public Path getPath() {
		if(path==null) {
			//如果在Window下,用dev路径,如果在其他系统,则用生产环境路径prodPath by zhengkai.blog.csdn.net
			if(System.getProperty("os.name").toLowerCase().startsWith("win")) {
				path = Paths.get(devPath);
			}else {
				path = Paths.get(prodPath);
			}
		}
		return path;
	}


	@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, getPath().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(getPath(), 1)
					.filter(path -> !path.equals(getPath()))
					.map(getPath()::relativize);
		}
		catch (IOException e) {
			throw new StorageException("Failed to read stored files", e);
		}

	}

	@Override
	public Path load(String filename) {
		return getPath().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(getPath().toFile());
	}

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

StorageController

文件存储控制器包含以下REST API方法:

  • GET / 文件上传页面,基于Bootstrap+Freemarker,通过storageService.loadAll()浏览存储目录文件功能,通过MvcUriComponentsBuilder.fromMethodName映射文件提供下载功能(关于该功能更多详情请见附录部分)。
  • GET /files/{filename} 文件下载URL,如果文件存在则下载,然后返回"Content-Disposition:attachment; filename=/.../"的响应头Response Header,在浏览器中触发文件下载。
  • POST / 文件上传方法,通过StorageService.store(file)提供上传功能。

import java.io.IOException;
import java.util.stream.Collectors;

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.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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.alibaba.fastjson.JSON;
import com.softdev.system.demo.entity.StorageFileNotFoundException;
import com.softdev.system.demo.service.StorageService;


@RestController
@RequestMapping("/storage")
/**
 * SpringBoot2FileUpload文件上传
 * @author zhengkai.blog.csdn.net
 * */
public class StorageController {

	@Autowired
	private StorageService storageService;

	@GetMapping("/files")
	public ModelAndView listUploadedFiles(ModelAndView modelAndView) throws IOException {
		//返回目录下所有文件信息
		modelAndView.addObject("files", storageService.loadAll().map(
				path -> MvcUriComponentsBuilder.fromMethodName(StorageController.class,
						"serveFile", path.getFileName().toString()).build().toString())
				.collect(Collectors.toList()));
		//返回目录信息
		modelAndView.addObject("path",storageService.getPath());
		modelAndView.setViewName("uploadForm");
		//查看ModelAndView包含的内容
		System.out.println(JSON.toJSONString(modelAndView));
		return modelAndView;
	}

	@GetMapping("/files/{filename:.+}")
	@ResponseBody
	public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
		//加载文件
		Resource file = storageService.loadAsResource(filename);
		log.info("download file:"+file.getFilename());
		//attachment附件下载模式,直接下载文件
		return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
				"attachment; filename=\"" + URLEncoder.encode(file.getFilename(), "UTF-8") + "\"").body(file);
	}

	@PostMapping("/files")
	public ModelAndView handleFileUpload(@RequestParam("file") MultipartFile file,
			RedirectAttributes redirectAttributes) {
		//存储文件
		storageService.store(file);
		//返回成功消息
		redirectAttributes.addFlashAttribute("message",
				"恭喜你,文件" + file.getOriginalFilename() + "上传成功!");
		return new ModelAndView("redirect:/storage/files");
	}

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

}

Html

<html>
<head>
	<meta charset="utf-8">
	
	
    <link href="//cdn.staticfile.org/twitter-bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet">
    
    <script src="//cdn.staticfile.org/jquery/3.3.1/jquery.min.js">script>
    
    <script src="//cdn.staticfile.org/twitter-bootstrap/4.1.1/js/bootstrap.min.js">script>
head>
<body>
	<#if message??>
		${message!!}
	#if>

	<div>
		
		<form method="POST" enctype="multipart/form-data" action="${request.contextPath}/storage/files">
			<table>
				<tr><td><input value="选择文件" type="file" name="file" class="btn btn-primary"/>td>tr>
				<tr><td><input type="submit" value="上传" class="btn btn-primary"/>td>tr>
			table>
		form>
		
	div>
	<div>
		<p>存储目录${path!!}有以下文件,可点击下载:p>
		<div class="list-group">
		   <#list files as file>
		     <a href="${file!!}" class="list-group-item list-group-item-action">${file!!}a>
		   #list >
		div>
	div>

body>
html>

效果展示

http://localhost:9999/fileupload/storage/files

SpringBoot2FileUpload(SpringBoot2官方文件上传下载DEMO)_第2张图片

UriComponentsBuilder.fromMethodName

SpringMvc4提供的新功能,MvcUriComponentsBuilder官方DOC文档,功能如下:

方法 功能
UriComponentsBuilder.fromMethodName(UriComponentsBuilder builder, Class controllerType, String methodName, Object… args) 通过Controller(控制器名或类)和Method(是方法名不是mapping名),映射该方法到URL上面

例如上文DEMO中
MvcUriComponentsBuilder.fromMethodName(StorageController.class,"serveFile", path.getFileName().toString())

代表
http://domain:post(application.yml的server.port) +
/basePath(application.yml的server.servlet.context-path) +
/Controller(根据控制器名或类找到对应的Mapping名) +
/Method的Mapping(例如“serveFile”method的mapping是@GetMapping("/files/{filename:.+}"),则附加上Object... args所有的参数进去作为参数,获得最终的url)

得到最终访问该方法的url。

你可能感兴趣的:(Spring,SpringBoot2启示录)