Пример Upload file c Spring Boot и AngularJS

View more Tutorials:

1- Цель статьи

В данной статье я покажу вам как создать приложение Upload File исопльзуя  Spring Boot и AngularJS, ниже является изображение для предварительного просмотра приложениея, которое вы выполним:
Оповестить на интерфейсе когда происходит ошибка загрузки:
Отобразить список загруженных файлов, и обработать скачивание когда пользователь кликает на ссылку (link).

2- Создать проект Spring Boot

На  Eclipse создать проект  Spring Boot:
pom.xml
<?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>org.o7planning</groupId>
    <artifactId>SpringBootFileUploadAngularJS</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringBootFileUploadAngularJS</name>
    <description>Spring Boot + File Upload + AngularJS</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.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-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
SpringBootFileUploadAngularJsApplication.java
package org.o7planning.sbfileupload;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootFileUploadAngularJsApplication {

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

}

3- Form, Controller, Exception Handler

Класс  UploadForm представляет данные формы  HTML.
UploadForm.java
package org.o7planning.sbfileupload.form;

import org.springframework.web.multipart.MultipartFile;

public class UploadForm {

    private String description;

    private MultipartFile[] files;

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public MultipartFile[] getFiles() {
        return files;
    }

    public void setFiles(MultipartFile[] files) {
        this.files = files;
    }

}
MainController.java
package org.o7planning.sbfileupload.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {

    @GetMapping("/")
    public String index() {
        return "upload";
    }

}
Класс  MainRESTController определяет  REST API чтобы обрабатывать данные файла загруженного пользователем. Данный  REST API будет вызван с помощью  AngularJS (Смотрите в  UploadFileCtrl.js).
MainRESTController.java
package org.o7planning.sbfileupload.restcontroller;

import java.io.File;
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.util.ArrayList;
import java.util.List;

import org.o7planning.sbfileupload.form.UploadForm;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class MainRESTController {

    // Linux: /home/{user}/test
    // Windows: C:/Users/{user}/test
    private static String UPLOAD_DIR = System.getProperty("user.home") + "/test";
 
    @PostMapping("/rest/uploadMultiFiles")
    public ResponseEntity<?> uploadFileMulti(@ModelAttribute UploadForm form) throws Exception {

        System.out.println("Description:" + form.getDescription());

        String result = null;
        try {

            result = this.saveUploadedFiles(form.getFiles());

        }
        // Here Catch IOException only.
        // Other Exceptions catch by RestGlobalExceptionHandler class.
        catch (IOException e) {
            e.printStackTrace();
            return new ResponseEntity<>("Error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
        }

        return new ResponseEntity<String>("Uploaded to: " + result, HttpStatus.OK);
    }

    // Save Files
    private String saveUploadedFiles(MultipartFile[] files) throws IOException {

        // Make sure directory exists!
        File uploadDir = new File(UPLOAD_DIR);
        uploadDir.mkdirs();

        StringBuilder sb = new StringBuilder();

        for (MultipartFile file : files) {

            if (file.isEmpty()) {
                continue;
            }
            String uploadFilePath = UPLOAD_DIR + "/" + file.getOriginalFilename();

            byte[] bytes = file.getBytes();
            Path path = Paths.get(uploadFilePath);
            Files.write(path, bytes);

            sb.append(uploadFilePath).append(", ");
        }
        return sb.toString();
    }

    @GetMapping("/rest/getAllFiles")
    public List<String> getListFiles() {
        File uploadDir = new File(UPLOAD_DIR);

        File[] files = uploadDir.listFiles();

        List<String> list = new ArrayList<String>();
        for (File file : files) {
            list.add(file.getName());
        }
        return list;
    }

    // @filename: abc.zip,..
    @GetMapping("/rest/files/{filename:.+}")
    public ResponseEntity<Resource> getFile(@PathVariable String filename) throws MalformedURLException {
        File file = new File(UPLOAD_DIR + "/" + filename);
        if (!file.exists()) {
            throw new RuntimeException("File not found");
        }
        Resource resource = new UrlResource(file.toURI());
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
                .body(resource);
    }

}
По умолчанию размер файла загруженного на Server не должен превышать  1MB. И если пользователь загружает несколько файлов одновременно, общий размер так же не должен превышать  1MB. Но вы можете конфигурировать, чтобы изменить данные параметры.
application.properties
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=5MB

spring.thymeleaf.cache=false
RestGlobalExceptionHandler это кастомизированный класс, расширенный из класса ResponseEntityExceptionHandler. В данном классе вы можете обрабатывать исключения выброшенные (throw) из методов  REST. Это поможет вам обрабатывать исключения централизованно, вместо обрабатывания исключения в каждом методе  REST.
RestGlobalExceptionHandler.java
package org.o7planning.sbfileupload.exceptionhandler;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
 
import javax.servlet.http.HttpServletRequest;
 
@ControllerAdvice
public class RestGlobalExceptionHandler extends ResponseEntityExceptionHandler {
 
    // Catch max file size Exception.
    @ExceptionHandler(MultipartException.class)
    @ResponseBody
    public ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
 
        HttpStatus status = this.getStatus(request);
        return new ResponseEntity<String>("(Message in RestGlobalExceptionHandler *): " + ex.getMessage(), status);
    }
 
    // Catch Other Exception
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseEntity<?> handleControllerRootException(HttpServletRequest request, Throwable ex) {
 
        HttpStatus status = this.getStatus(request);
        return new ResponseEntity<String>("(Message in RestGlobalExceptionHandler **): " + ex.getMessage(), status);
    }
 
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }
 
}

4- Javascript & View (Thymeleaf)

upload.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
   <head>
      <title>Spring Boot File Upload with AngularJS</title>
      <meta charset="utf-8" />
      <!-- Check other AngularJS version at: -->
      <!-- https://code.angularjs.org/1.6.9/docs/misc/downloading -->
      <script
         src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
      <script src="/js/MainApp.js"></script>
      <script src="/js/UploadFileCtrl.js"></script>
      <script src="/js/GetFilesCtrl.js"></script>
   </head>
   <body ng-app="MainApp">
      <h2>Spring Boot File Upload with AngularJS</h2>
      <div ng-controller="UploadFileController">
         <form>
            Description: <br/>
            <input type="text" name="description" ng-model="myForm.description" style="width:350px;"/>
            <br/><br/>                 
            File to upload (1): <input type="file" file-model="myForm.files[0]"/><br />      
            File to upload (2): <input type="file" file-model="myForm.files[1]"/><br />    
            File to upload (3): <input type="file" file-model="myForm.files[2]"/><br />    
            File to upload (4): <input type="file" file-model="myForm.files[3]"/><br />    
            File to upload (5): <input type="file" file-model="myForm.files[4]"/><br />    
            <button type="button" ng-click="doUploadFile()">Upload</button>
         </form>
         <h2>Upload Results:</h2>
         <div style="border:1px solid #ccc;padding: 5px;">
            <span ng-bind="uploadResult"></span>
         </div>
      </div>
      <!-- Get Files -->
      <hr>
      <div ng-controller="GetFilesController">
         <button type="button" ng-click="getAllFiles()">Get All Files</button>
         <ul>
            <li ng-repeat="file in allFiles">
               <a href='/rest/files/{{file}}'>{{file}}</a>
            </li>
         </ul>
      </div>
   </body>
</html>
В AngularJS, использование атрибута (attribute) ng-model поможет вам с двусторонней привязкой (2-way binding) между элемент  Input у Form и Model, что значит если данные на  Model меняются то интерфейс (элемент  Input) будет обновлен, и наоборот, если пользователь меняет на интерфейсее (элемент  Input), то  Model будет обновлен.
К сожалению атрибут (attribute) ng-model не поддерживает двусторонню привязку между  Model и Input[file], поэтому вам нужно определить directive (директива) с названием  "fileModel" чтобы построить двустороннюю привязку между Model и Input[file]. Данная  Directive определена в  MainApp.js:
js/MainApp.js
// main app.
var mainApp = angular.module('MainApp', []);
 
// DIRECTIVE - FILE MODEL
mainApp.directive('fileModel', ['$parse', function ($parse) {
    return {
       restrict: 'A',
       link: function(scope, element, attrs) {
          var model = $parse(attrs.fileModel);
          var modelSetter = model.assign;
          
          element.bind('change', function(){
             scope.$apply(function(){
                modelSetter(scope, element[0].files[0]);
             });
          });
       }
    };
    
}]);
Файл  UploadFileCtrl.js содержит функции  AngularJS управляющие загрузку файлов на Server.
js/UploadFileCtrl.js
// CONTROLLER UPLOAD FILE
mainApp.controller('UploadFileController', function($scope, $http) {

    $scope.uploadResult ="";
    
    $scope.myForm = {
        description: "",
        files: []
    }

    $scope.doUploadFile = function() {  

        var url = "/rest/uploadMultiFiles";


        var data = new FormData();

        data.append("description", $scope.myForm.description);
        for (i = 0; i < $scope.myForm.files.length; i++) {
            data.append("files", $scope.myForm.files[i]);
        }

        var config = {
            transformRequest: angular.identity,
            transformResponse: angular.identity,
            headers: {
                'Content-Type': undefined
            }
        }
       

        $http.post(url, data, config).then(
            // Success
            function(response) {
                $scope.uploadResult =  response.data;
            },
            // Error
            function(response) {
                $scope.uploadResult = response.data;
            });
    };

});
Файл  GetFilesCtrl.js содержит файл  AngularJS​​​​​​​, управляющий получение списка файлов, загруженных на  Server.

js/GetFilesCtrl.js
mainApp.controller('GetFilesController', function($scope, $http) {

    $scope.allFiles = [];


    $scope.getAllFiles = function() {  

        // REST URL:
        var url = "/rest/getAllFiles";
        $http.get(url).then(
            // Success
            function(response) { alert("OK");
                $scope.allFiles = response.data;
            },
            // Error
            function(response) {
                alert("Error: " + response.data);
            }
        );
    };
});

View more Tutorials: