Spring Boot + тимелеаф реализует загрузку и загрузку файлов

Spring Boot Spring

Недавно коллега спросил меня, есть ли у меня электронные книги по технике, я открыл маленькую библиотеку на компьютере, но письма ему были слишком большими, а компания запретила расшаривание с папками, поэтому я потратил полдня на написание небольшая программа для загрузки файлов, развернутая на собственной Linux-машине.

Обеспечивает функциональность:1. Загрузка файла 2. Отображение и загрузка списка файлов

Первоначальная загрузка уродлива, я написал код js для ее оптимизации, и окончательный интерфейс выглядит следующим образом:

上传文件

Сначала приводятся результаты, а далее приводится пошаговая демонстрация того, как их достичь.

1. Новый проект

Прежде всего, конечно, создайте новый проект spring-boot.Вы можете инициализировать проект на веб-сайте или использовать функцию Spring Initialier среды IDE для создания нового проекта. Здесь я создаю новый проект из IDEA:

Следующий шаг, затем введите группу и артефакт, продолжайте нажимать «Далее»:

В это время появится интерфейс выбора модуля, щелкните веб-опцию, отметьте Веб, подтвердите, что это веб-приложение, а затем щелкните Механизмы шаблонов, чтобы выбрать интерфейсный механизм шаблонов.Мы выбираем Thymleaf, который также рекомендуется Spring- boot для замены jsp.

Последний шаг, затем дождитесь успешной инициализации проекта.

2.пом настройки

Сначала проверьте, какие зависимости нужно добавить в проект, и вставьте мой файл 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, последние две webjar интегрируют bootstrap и jquery, и будут использоваться другие коды.

Последний плагин Spring boot maven добавляется при создании системы и имеет следующие преимущества:

1. Он может упаковывать все jar-файлы в пути к классам и встраивать их в исполняемый файл «über-jar», что удобно для пользователей при передаче сервисов.

2. Автоматический поискpublic static void main()метод и помечен как исполняемый

3. В соответствии с версией spring-boot предоставьте встроенное объяснение зависимостей.

3. Загрузить файловый контроллер

Если вы просто используете SpringMVC для загрузки файлов, вам необходимо настроитьMultipartResolverбобы, или вweb.xmlнастроить<multipart-config>, но с помощью автоматической настройки spring-boot вам не нужно ничего делать. Напишите класс контроллера напрямую, мы находимся вsrc/main/javaСоздайте новый пакет контроллера и создайте новый 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Аннотация, подтверждающая, что это контроллер, добавленная перед каждым методом.@GetMappingи@PostMappingСоответствующие запросы Get и Post соответственно.

прежде всего@GetMapping("/"),методlistUploadedFiles, как следует из названия, отображает список файлов, здесь мы просматриваем все файлы в папке с помощью storageService и используем метод карты для извлечения списка ссылок и имен файлов и возвращаем массив объектов Linker. Объект Linker представляет собой простой pojo, который содержит только следующие две части:

private String fileUrl;
private String fileName;

Этот метод включает в себя использование Stream в Java 8. Если вы не понимаете, вы можете прочитать эту статьюПодробное объяснение возможностей Java8 (2) Stream API.

Далее@GetMapping("/files/{filename:.+}"), путьserveFile, Этот способ обеспечивает функцию скачивания файла, либо с помощью storageservice, код storageservice будет выложен позже. Наконец, используйте ResponseEntity, чтобы вернуть файл в виде тела запрашивающей стороне.

@PostMapping("/")изhandleFileUploadИспользуйте Post request для загрузки файлов, параметров@RequestParam("file")Извлеките файловый объект в запросе веб-страницы или используйте хранилищеService для сохранения объекта и, наконец, используйте перенаправление для обновления веб-страницы и сообщения об успешно загруженном сообщении.

4. Работа с файлами

Многие из методов, вызываемых контроллером выше, предоставляются 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());
    }
}

Этот класс также в основном использует NIO Java, используя объект Path для определения пути сохранения по умолчанию для файлов местоположения.

Первый взгляд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 для копирования файлового потока в путь, соответствующий местоположению.Конечно, мы также можем использовать метод transferTo для сохранения файла.file.transferTo(this.rootLocation.resolve(filename).toFile());

loadAllМетод загружает всю информацию о пути файла по пути,loadAsResourceЭто загрузить файл как объект ресурса, затем просмотреть код контроллера и, наконец, принять объект ресурса в качестве тела и вернуть его запрашивающей стороне.

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}" 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" />
</body>
</html>

Главное здесь<form>содержимое тега,<form method="POST" action="/" enctype="multipart/form-data">enctypeдолжно быть написано какmultipart/form-data,Используя POST для загрузки файлов, исходное управление загрузкой некрасиво, поэтому я сделал текст+ввод на поверхность, а невидимый ввод для загрузки файлов поставил ниже, код можете посмотреть сами, эта статья будет не длинной- замотанный.

Список также размещен ниже для отображения списка файлов.Здесь мы получаем объект компоновщика, предоставленный сервером, и мы можем непрерывно получать два элемента fileUrl и fileName в нем с помощью foreach.

Здесь jquery заменен на Microsoft 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);
	}
}

Говоря о том, что этот проект в основном готов к запуску, конечно, вы также можете добавить то, что вам нужно, чтобы продолжать его улучшать.