Spring Uploading Files 官方示例项目解析

该项目对于学习 Spring boot ,了解 Spring boot 项目文件上传,与一些 Java 新特性还不错

由于 Spring boot 项目的打包方式 ( jar ) 所以上传的目录通常是和项目分离

所以在访问资源的方式上有些区别 (与原 webapp 项目)

Application.java 项目启动类

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;

import hello.storage.StorageProperties;
import hello.storage.StorageService;

@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class Application {

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

	// Spring boot 提供 CommandLineRunner 在项目启动后加载一些内容
	@Bean
	CommandLineRunner init(StorageService storageService) {
		return (args) -> { // lambda 表达式
			storageService.deleteAll();
			storageService.init();
		};
	}
}

FileUploadController.java 控制器

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.stereotype.Controller;
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 hello.storage.StorageFileNotFoundException;
import hello.storage.StorageService;

@Controller
public class FileUploadController {

	private final StorageService storageService;

	// 注入 storageService bean
	@Autowired
	public FileUploadController(StorageService storageService) {
		this.storageService = storageService;
	}

	// 上传文件列表
	@GetMapping("/")
	public String listUploadedFiles(Model model) throws IOException {

		// loadAll 方法返回一个 Stream<Path> 集合
		// Stream 是 Java 8 中对集合 Collection 的增强, 结合 lambda 用函数式编程对集合进行复杂的操作 ( 查找、遍历、过滤等)
		model.addAttribute("files", storageService.loadAll()
				// 解析这一行看起来复杂的代码
				// Stream.map 将一个 Stream 使用给定的转换函数 (Lambda) 映射为一个新的 Stream
				// 参数 path 即为 Stream<Path> 中的 Path
				// fromMethodName 方法创建一个 UriComponentsBuilder 对象 (通过 controller 方法名上的 mapping 路径与参数组数)
				// 返回的 UriComponentsBuilder 的 build 方法 创建一个 UriComponents 对象, 最后将 UriComponents 转换为字符串
				// Stream.collect 方法将 map 转换后的新 Stream 变为 list 返回给模板
				// 这一系列操作,实际上就是为了把 Stream<Path> 转换为一个存储 url 字符串的列表对象
				.map(path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class, "serveFile", path.getFileName().toString()).build().toString())
				.collect(Collectors.toList()));

		return "uploadForm";
	}

	// 文件下载
	// 正则表达式匹配, 语法: {varName:regex} 前面式变量名,后面式表达式
	// 匹配出现过一次或多次.的字符串 如: "xyz.png"
	@GetMapping("/files/{filename:.+}")
	@ResponseBody
	public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
		// 根据文件名读取文件
		Resource file = storageService.loadAsResource(filename);
		// @ResponseBody 用于直接返回结果(自动装配)
		// ResponseEntity 可以定义返回的 HttpHeaders 和 HttpStatus (手动装配)
		// ResponseEntity.ok 相当于设置 HttpStatus.OK (200)
		// CONTENT_DISPOSITION 该 标志将通知浏览器启动下载
		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 添加一个重定向参数
		redirectAttributes.addFlashAttribute("message", "You successfully uploaded " + file.getOriginalFilename() + "!");

		return "redirect:/";
	}

	// 统一处理该 controller 异常
	@ExceptionHandler(StorageFileNotFoundException.class)
	public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
		return ResponseEntity.notFound().build();
	}

}

StorageService 服务层接口

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 服务层实现

import java.io.IOException;
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;

@Service
public class FileSystemStorageService implements StorageService {

	private final Path rootLocation;

	@Autowired
	public FileSystemStorageService(StorageProperties properties) {
		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);
			}
			// 从 file 输入流复制到目标位置
			Files.copy(file.getInputStream(), 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 {
			// 通过给定的目录和深度来遍历,返回 Stream (集合中包含给定的路径)
			// filter 过滤掉指定的路径
			// map 将路径处理为相对路径,如: rootLocation = "a/b" path = "a/b/c/img.png" relativize 后,结果为 "c/img.png"
			return Files.walk(this.rootLocation, 1).filter(path -> !path.equals(this.rootLocation)).map(path -> this.rootLocation.relativize(path));
		} catch (IOException e) {
			throw new StorageException("Failed to read stored files", e);
		}

	}

	@Override
	public Path load(String filename) {
		// 组合一个新的 Path 对象, 如: filename = "gus" rootLocation="a/b", 执行后结果为 "a/b/gus"
		return rootLocation.resolve(filename);
	}

	@Override
	public Resource loadAsResource(String filename) {
		try {
			Path file = load(filename);
			// file.toUri 将 Path 转换为 uri 
			// 如: path = "upload-dir/1.jpg" toUrl 后结果为 "file:///home/maiyo/project/upload-files/upload-dir/1.jpg"
			// 通过 UrlResource 创建一个 Srping Resource 对象
			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);
		}
	}
}

StorageException 自定义异常类

//自定义异常类
public class StorageException extends RuntimeException {

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

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

StorageFileNotFoundException 自定义异常类,继承自 StorageException

// 自定义异常类
public class StorageFileNotFoundException extends StorageException {

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

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值