Spring MVC/Spring Boot实现图片文件上传和显示
1、第一步、在eclipse中构建一个标准的Maven工程
工程的信息:
<groupId>fantasy</groupId>
<artifactId>image</artifactId>


2、为工程添加Spring MVC和Spring boot相关的依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

3、创建文件存储服务的接口和实现类,对处理WEB页面的文件上传服务
1)文件存储服务接口类:StorageService.java
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();
}
2)文件存储服务接口实现类:FileSystemStorageService.java
package image.storage;
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;
/**
* 文件系统存储服务
* @author fantasy
*
*/
@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("File is empty: " + filename);
}
if (filename.contains("..")) {
// 文件路径安全校验
throw new StorageException(
"不能将文件保存到相对目录路径中: "
+ filename);
}
//将上传的文件保存到指定位置
Files.copy(file.getInputStream(), this.rootLocation.resolve(filename),
StandardCopyOption.REPLACE_EXISTING);
}
catch (IOException e) {
throw new StorageException("上传文件失败 " + filename, e);
}
}
/**
* 加载所有的文件路径
*/
@Override
public Stream<Path> loadAll() {
try {
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) {
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);
}
}
}
3)文件存储处理的异常类
StorageException.java
package image.storage;
/**
* 文件存储异常运行时处理类
* @author fantasy
*
*/
public class StorageException extends RuntimeException {
private static final long serialVersionUID = 2430191988074222554L;
public StorageException(String message) {
super(message);
}
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}
4) 文件存储异常类:StorageFileNotFoundException.java
package image.storage;
public class StorageFileNotFoundException extends StorageException {
private static final long serialVersionUID = -7119518537629449580L;
public StorageFileNotFoundException(String message) {
super(message);
}
public StorageFileNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

4、创建一个文件存储属性配置类,实现文件存储的相关自定义属性配置,如文件
上传的目录名称:StorageProperties.java
该类指定了文件上传的目录位置为:upload-dir
package image.storage;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("storage")
public class StorageProperties {
/**
* Folder location for storing files
*/
private String location = "upload-dir";
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}

5、实现文件上传控制器,这个是整个WEB应用的的核心,实现WEB页面到文件存储服务的联接控制。
文件名:FileUploadController.java
package image;
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.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 image.storage.StorageFileNotFoundException;
import image.storage.StorageService;
/**
* 文件上传服务控制器
* @author Fantasy
*
*/
@Controller
public class FileUploadController {
private final StorageService storageService;
@Autowired
public FileUploadController(StorageService storageService) {
this.storageService = storageService;
}
//文件上传的url入口:/upload,然后转入templates目录下的uploadForm.html文件
@GetMapping("/upload")
public String showUploadPage(Model model) throws IOException {
return "uploadForm";
}
//处理post上传文件的url:/upload,处理成功后页面跳转掉“/result”
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
storageService.store(file);
redirectAttributes.addFlashAttribute("message",
"文件: " + file.getOriginalFilename() + "上传成功!");
return "redirect:/result";
}
//处理url:/result的请求,显示templates目录下的uploadResult.html文件
//显示上传的文件列表
@GetMapping("/result")
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()));
System.out.println(model);
return "uploadResult";
}
//查看已经上传的文件
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
Resource file = storageService.loadAsResource(filename);
return ResponseEntity.ok(file);
}
//图片文件的路径是虚拟路径,如何在页面中直接显示呢?
@GetMapping("img/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> getFile(@PathVariable String filename)
{
Resource file = storageService.loadAsResource(filename);
return ResponseEntity.ok(file);
}
@ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
}
}
6、为页面创建静态资源目录,存储静态的html、js,css、图片等文件
如在项目中需要用的jquery则需要将jquery的库文件放在工程的静态目录中:
E:\workspace\image\src\main\resources\static\js\jquery-3.2.1.min.js
在这个工程中我们将index.html和jquery-3.2.1.min.js放在静态文件目录下。

7、为Spring MVC提供thymeleaf模板的WEB页面文件,提供文件上传的页面和文件展现的页面;
1)文件上传页面:该页面展示了如何引用静态js文件
uploadForm.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<!-- 静态文件css,js等放在static目录下 -->
<script src="./js/jquery-3.2.1.min.js"></script>
<script>
$(document).ready(function(){
alert("Upload File Page");
});
</script>
</head>
<body>
<div>
<form method="POST" enctype="multipart/form-data" action="/upload">
<table>
<tr><td>File to upload:</td><td><input type="file" name="file" accept="image/*"/></td></tr>
<tr><td></td><td><input type="submit" value="Upload" /></td></tr>
</table>
</form>
</div>
</body>
</html>
2)文件上传结果展示页面:uploadResult.html
注意:上传的文件是图片文件时才可以在页面上正常显示的
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:if="${message}">
<h2 th:text="${message}"/>
</div>
<div>
<ul>
<li th:each="file : ${files}">
<img th:src="${file}"/>
<a th:href="${file}" th:text="${file}" />
</li>
</ul>
</div>
</body>
</html>

8、为Spring应用设置相关的属性,如文件上传的大小,避免出现大坑
在resources目录下创建spring的属性配置文件application.properties
spring.http.multipart.max-file-size=128MB
spring.http.multipart.max-request-size=128MB

9、最后创建Spring boot的应用程序入口Application.java
package image;
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 image.storage.StorageProperties;
import image.storage.StorageService;
@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
CommandLineRunner init(StorageService storageService)
{
return (args)->{
storageService.deleteAll();
storageService.init();
};
}
}
10、运行application.java类,启动spring boot应用
10:53:55.672 [main] DEBUG org.springframework.boot.devtools.settings.DevToolsSettings - Included patterns for restart : []
10:53:55.674 [main] DEBUG org.springframework.boot.devtools.settings.DevToolsSettings - Excluded patterns for restart : [/spring-boot-starter/target/classes/, /spring-boot-autoconfigure/target/classes/, /spring-boot-starter-[\w-]+/, /spring-boot/target/classes/, /spring-boot-actuator/target/classes/, /spring-boot-devtools/target/classes/]
10:53:55.674 [main] DEBUG org.springframework.boot.devtools.restart.ChangeableUrls - Matching URLs for reloading : [file:/E:/workspace/image/target/classes/]
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.9.RELEASE)
2018-01-07 10:53:55.950 INFO 2932 --- [ restartedMain] image.Application : Starting Application on V3EQ67T1ANYZUSN with PID 2932 (E:\workspace\image\target\classes started by Administrator in E:\workspace\image)
2018-01-07 10:53:55.950 INFO 2932 --- [ restartedMain] image.Application : No active profile set, falling back to default profiles: default
2018-01-07 10:53:56.193 INFO 2932 --- [ restartedMain] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1f8de44: startup date [Sun Jan 07 10:53:56 CST 2018]; root of context hierarchy
2018-01-07 10:53:57.719 INFO 2932 --- [ restartedMain] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2018-01-07 10:53:57.734 INFO 2932 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-01-07 10:53:57.736 INFO 2932 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-01-07 10:53:57.862 INFO 2932 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-01-07 10:53:57.862 INFO 2932 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1670 ms
2018-01-07 10:53:58.020 INFO 2932 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2018-01-07 10:53:58.025 INFO 2932 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-01-07 10:53:58.025 INFO 2932 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-01-07 10:53:58.026 INFO 2932 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-01-07 10:53:58.026 INFO 2932 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-01-07 10:53:58.361 INFO 2932 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1f8de44: startup date [Sun Jan 07 10:53:56 CST 2018]; root of context hierarchy
2018-01-07 10:53:58.435 INFO 2932 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/img/{filename:.+}],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.core.io.Resource> image.FileUploadController.getFile(java.lang.String)
2018-01-07 10:53:58.436 INFO 2932 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/upload],methods=[GET]}" onto public java.lang.String image.FileUploadController.showUploadPage(org.springframework.ui.Model) throws java.io.IOException
2018-01-07 10:53:58.440 INFO 2932 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/upload],methods=[POST]}" onto public java.lang.String image.FileUploadController.handleFileUpload(org.springframework.web.multipart.MultipartFile,org.springframework.web.servlet.mvc.support.RedirectAttributes)
2018-01-07 10:53:58.440 INFO 2932 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/files/{filename:.+}],methods=[GET]}" onto public org.springframework.http.ResponseEntity<org.springframework.core.io.Resource> image.FileUploadController.serveFile(java.lang.String)
2018-01-07 10:53:58.441 INFO 2932 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/result],methods=[GET]}" onto public java.lang.String image.FileUploadController.listUploadedFiles(org.springframework.ui.Model) throws java.io.IOException
2018-01-07 10:53:58.443 INFO 2932 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-01-07 10:53:58.443 INFO 2932 --- [ restartedMain] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-01-07 10:53:58.473 INFO 2932 --- [ restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-01-07 10:53:58.473 INFO 2932 --- [ restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-01-07 10:53:58.516 INFO 2932 --- [ restartedMain] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-01-07 10:53:59.020 INFO 2932 --- [ restartedMain] oConfiguration$WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2018-01-07 10:53:59.241 INFO 2932 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2018-01-07 10:53:59.328 INFO 2932 --- [ restartedMain] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-01-07 10:53:59.419 INFO 2932 --- [ restartedMain] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-01-07 10:53:59.426 INFO 2932 --- [ restartedMain] image.Application : Started Application in 3.737 seconds (JVM running for 4.758)

11、在浏览器中进行测试web应用,推荐使用chrome或极速浏览器,windows 10的edge浏览器不支持的

