Spring Boot + thymeleaf 实现文件上传下载功能

作者:卡巴拉的树 时间:2022-05-22 03:56:13 

最近同事问我有没有有关于技术的电子书,我打开电脑上的小书库,但是邮件发给他太大了,公司又禁止用文件夹共享,于是花半天时间写了个小的文件上传程序,部署在自己的Linux机器上。

提供功能: 1 .文件上传 2.文件列表展示以及下载

原有的上传那块很丑,写了点js代码优化了下,最后界面显示如下图:

Spring Boot + thymeleaf 实现文件上传下载功能 

先给出成果,下面就一步步演示怎么实现。

1.新建项目

首先当然是新建一个spring-boot工程,你可以选择在网站初始化一个项目或者使用IDE的Spring Initialier功能,都可以新建一个项目。这里我从IDEA新建项目:

Spring Boot + thymeleaf 实现文件上传下载功能 

下一步,然后输入group和artifact,继续点击next:

Spring Boot + thymeleaf 实现文件上传下载功能 

这时候出现这个模块选择界面,点击web选项,勾上Web,证明这是一个webapp,再点击Template Engines选择前端的模板引擎,我们选择Thymleaf,spring-boot官方也推荐使用这个模板来替代jsp。

Spring Boot + thymeleaf 实现文件上传下载功能 Spring Boot + thymeleaf 实现文件上传下载功能

最后一步,然后等待项目初始化成功。

Spring Boot + thymeleaf 实现文件上传下载功能 

2.pom设置

首先检查项目需要添加哪些依赖,直接贴出我的pom文件:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.shuqing28</groupId>
<artifactId>upload</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>upload</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-configuration-processor</artifactId>
 <optional>true</optional>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.webjars/bootstrap -->
<dependency>
 <groupId>org.webjars</groupId>
 <artifactId>bootstrap</artifactId>
 <version>3.3.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.webjars.bower/jquery -->
<dependency>
 <groupId>org.webjars.bower</groupId>
 <artifactId>jquery</artifactId>
 <version>2.2.4</version>
</dependency>
</dependencies>
<build>
<plugins>
 <plugin>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-maven-plugin</artifactId>
 </plugin>
</plugins>
</build>
</project>

可以查看到 spring-boot-starter-thymeleaf 包含了webapp,最后两个webjars整合了bootstrap和jquery,其它的等代码里用到再说。

最后一个Spring boot maven plugin是系统创建时就添加的,它有以下好处:

1 . 它能够打包classpath下的所有jar,构建成一个可执行的“über-jar”,方便用户转移服务

2 . 自动搜索 public static void main() 方法并且标记为可执行类

3 . 根据spring-boot版本,提供内建的依赖解释。

3. 上传文件控制器

如果你只是使用SpringMVC上传文件,是需要配置一个 MultipartResolver 的bean的,或者在 web.xml 里配置一个 <multipart-config> ,不过借助于spring-boot的自动配置,你什么都不必做。直接写控制器类,我们在 src/main/java 下新建controller的package,并且新建FileUploadController:


package com.shuqing28.upload.controller;
import com.shuqing28.uploadfiles.pojo.Linker;
import com.shuqing28.uploadfiles.exceptions.StorageFileNotFoundException;
import com.shuqing28.uploadfiles.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.io.IOException;
import java.util.List;
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)throws IOException {
   List<Linker> linkers = storageService.loadAll().map(
       path -> new Linker(MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
           "serveFile", path.getFileName().toString()).build().toString(),
           path.getFileName().toString())
   ).collect(Collectors.toList());
   model.addAttribute("linkers", linkers);
   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) {
   return ResponseEntity.notFound().build();
 }
}

类定义处添加了 @Controller 注解,证明这是一个Controller,每个方法前添加了 @GetMapping 和 @PostMapping 分别相应Get和Post请求。

首先是 @GetMapping("/") ,方法 listUploadedFiles ,顾名思义,显示文件列表,这里我们借助于storageService遍历文件夹下的所有文件,并且用map方法提合成了链接和文件名列表,返回了一个Linker对象的数组,Linker对象是一个简单pojo,只包含下面两部分:


private String fileUrl;
private String fileName;

这个方法包含了对Java8中Stream的使用,如果有不理解的可以看看这篇文章 Java8 特性详解(二) Stream API .

接下来是 @GetMapping("/files/{filename:.+}") ,方法是 serveFile ,该方法提供文件下载功能,还是借助于storageservice,后面会贴出storageservice的代码。最后使用ResponseEntity,把文件作为body返回给请求方。

@PostMapping("/") 的 handleFileUpload 使用Post请求来上传文件,参数 @RequestParam("file") 提取网页请求里的文件对象,还是使用storageService来保存对象,最后使用重定向来刷新网页,并且给出成功上传的message。

4. 文件处理

上面Controller调用的很多方法由StorageService提供,我们定义一个接口,包含以下方法:


package com.shuqing28.uploadfiles.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();
}

因为我这里只是借助于本地文件系统处理文件的长传下载,所以有了以下实现类:


package com.shuqing28.uploadfiles.service;
import com.shuqing28.uploadfiles.exceptions.StorageException;
import com.shuqing28.uploadfiles.exceptions.StorageFileNotFoundException;
import com.shuqing28.uploadfiles.config.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.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 FileSystemStorageService implements StorageService {
 private final Path rootLocation;
 @Autowired
 public FileSystemStorageService(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 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);
     }
     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 {
     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());
 }
}

这个类也基本运用了Java的NIO,使用Path对象定义了location用于文件的默认保存路径。

先看 store 方法,store接受一个MultipartFile对象作为参数,想比于传统JSP中只是传二进制字节数组,MultipartFile提供了很多方便调用的方法让我们可以获取到上传文件的各项信息:


public interface MultipartFile extends InputStreamSource {
String getName();
String getOriginalFilename();
String getContentType();
boolean isEmpty();
long getSize();
byte[] getBytes() throws IOException;
InputStream getInputStream() throws IOException;
void transferTo(File dest) throws IOException, IllegalStateException;
}

代码里使用了Files的copy方法把文件流拷到location对应的Path里,当然我们也可以使用transferTo方法保存文件, file.transferTo(this.rootLocation.resolve(filename).toFile());

loadAll方法加载该路径下的所有文件Path信息, loadAsResource 则是加载文件为一个Resource对象,再看Controller的代码,最后是接受一个Resource对象作为body返回给请求方。

5. 前端模板

最后定义了前端模板,这里依旧先看代码:


<html xmlns:th="http://www.thymeleaf.org">
<head>
 <title>Share Files</title>
</head>
<body>
<div class="col-md-8 col-md-offset-2" th:if="${message}">
 <h2 th:text="${message}"/>
</div>
<div class="col-md-8 col-md-offset-2">
 <form method="POST" action="/" enctype="multipart/form-data">
   <!-- COMPONENT START -->
   <input type="file" name="file" class="input-ghost" style="visibility:hidden; height:0"/>
   <div class="form-group">
     <div class="input-group input-file" name="Fichier1">
       <input type="text" class="form-control" placeholder='Choose a file...'/>
       <span class="input-group-btn">
         <button class="btn btn-default btn-choose" type="button">Choose</button>
    </span>
     </div>
   </div>
   <!-- COMPONENT END -->
   <div class="form-group">
     <button type="submit" class="btn btn-primary pull-right">Submit</button>
     <button type="reset" class="btn btn-danger">Reset</button>
   </div>
 </form>
</div>
<div class="col-md-8 col-md-offset-2">
 <ul>
   <li th:each="linker: ${linkers}">
     <a th:href="${linker.fileUrl}" rel="external nofollow" th:text="${linker.fileName}" />
   </li>
 </ul>
</div>
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.1.min.js"></script>
<script src="/webjars/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script type="text/javascript" th:inline="javascript">
 function bs_input_file() {
   $(".input-file").before(
     function() {
       if ( ! $(this).prev().hasClass('input-ghost') ) {
         var element = $(".input-ghost");
         element.change(function(){
           element.next(element).find('input').val((element.val()).split('\\').pop());
         });
         $(this).find("button.btn-choose").click(function(){
           element.click();
         });
         $(this).find("button.btn-reset").click(function(){
           element.val(null);
           $(this).parents(".input-file").find('input').val('');
         });
         $(this).find('input').css("cursor","pointer");
         $(this).find('input').mousedown(function() {
           $(this).parents('.input-file').prev().click();
           return false;
         });
         return element;
       }
     }
   );
 }
 $(function() {
   bs_input_file();
 });
</script>
<link rel="stylesheet" href="/webjars/bootstrap/3.3.5/css/bootstrap.min.css" rel="external nofollow" />
</body>
</html>

这里重要的地方还是 <form> 标签内的内容, <form method="POST" action="/" enctype="multipart/form-data"> enctype 一定要写成 multipart/form-data ,使用POST上传文件,原有的上传控件很丑,所以做了一个text+input放在表面,在下面放了一个隐形的上传文件的input,可以自己看看代码,本文就不啰嗦了。

下面还放了一个list用于展示文件列表,这里我们获取到服务端提供的linkers对象,不断foreach就可以获得里面的两个元素fileUrl和fileName。

这里jquery换成了微软的CDN,webjars的总是引入不进来,不知道什么原因。

其它设置

在 src/main/resources/application.properties 里设置上传文件大小限制


spring.http.multipart.max-file-size=128MB
spring.http.multipart.max-request-size=128MB

另外在``还设置了文件默认保存路径:


package com.shuqing28.uploadfiles.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("storage")
public class StorageProperties {
 private String location = "/home/jenkins/upload-files/";
 public String getLocation() {
   return location;
 }
 public void setLocation(String location) {
   this.location = location;
 }
}

这里注意,由于StorageProperties的设置,在Application的那个类中要添加上


@EnableConfigurationProperties注解
@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class UploadApplication {
public static void main(String[] args) {
SpringApplication.run(UploadApplication.class, args);
}
}

总结

以上所述是小编给大家介绍的Spring Boot + thymeleaf 实现文件上传下载功能网站的支持!

来源:https://juejin.im/post/5a326dcaf265da431048685e

标签:spring,boot,thymeleaf
0
投稿

猜你喜欢

  • Android View移动的六种方法小结

    2023-07-06 02:43:39
  • Java简化复杂系统调用的门面设计模式

    2023-06-08 10:38:28
  • SpringBoot项目速度提升之延迟初始化(Lazy Initialization)详解

    2021-11-27 06:37:22
  • Java中的传值与传引用实现过程解析

    2023-02-21 12:59:41
  • Android ListView实现上拉加载下拉刷新和滑动删除功能

    2021-09-24 19:08:04
  • Spring Data JPA中 in 条件参数的传递方式

    2023-11-08 06:45:47
  • C#语法相比其它语言比较独特的地方(二)

    2021-12-20 20:56:03
  • java实现发送邮件的示例代码

    2023-07-04 17:00:54
  • Java编程实现五子棋人人对战代码示例

    2021-12-27 12:58:51
  • Android NotificationListenerService 通知服务原理解析

    2022-03-29 05:34:29
  • Android开发解决popupWindow重叠报错问题

    2022-08-02 13:27:01
  • 最详细的文件上传下载实例详解(推荐)

    2021-12-12 08:18:13
  • C# 8.0中的范围类型(Range Type)示例详解

    2023-04-11 22:26:10
  • C# Stream 和 byte[] 之间的转换

    2023-06-24 23:14:52
  • Java为实体类动态添加属性的方法详解

    2023-09-01 05:31:43
  • Java中的5种同步辅助类介绍

    2023-11-14 21:36:28
  • 浅述int与string类型转换的两种方法

    2023-09-09 08:56:28
  • AOP从静态代理到动态代理(Emit实现)详解

    2023-11-27 14:49:56
  • 分享一个C#编写简单的聊天程序(详细介绍)

    2023-06-19 03:21:59
  • SpringBoot2使用JTA组件实现基于JdbcTemplate多数据源事务管理(亲测好用)

    2021-10-12 06:36:09
  • asp之家 软件编程 m.aspxhome.com