[JAVA] Comment réaliser un téléchargement de fichiers volumineux avec TERASOLUNA 5.x (= Spring MVC)

1.Tout d'abord

Cette fois, je vais vous expliquer comment télécharger un fichier avec une grande taille de données avec TERASOLUNA 5.x (= Spring MVC). Si la taille des données est petite, vous n'avez pas à vous en soucier, mais si elle est grande, vous devez faire attention aux ressources du serveur.

(point)

Pour le téléchargement, veuillez vous référer à "Comment réaliser un téléchargement de fichier énorme avec TERASOLUNA 5.x (= SpringMVC)".

2. Code source

2.1. Code médiocre

(Code incorrect 1) Lors de la prise d'une requête HTTP comme argument


@RequestMapping(path = "chunked", method = RequestMethod.POST)
public ResponseEntity<String> streamUpload(HttpServletRequest httpRequest) {
            
        // You should be getted 5 parameters from HttpServletRequest and validated it
            
        // omitted
}

Ceci est la méthode à l'ancienne familière avec ʻAction de ServletetStruts. Il reçoit HttpServletRequestcomme argument de la méthode du gestionnaire et lit les données du fichier téléchargé depuis BODY. L'inconvénient de cette méthode est qu'elle gère leHttpServletRequest` brut et n'est pas testable.

(Code incorrect 2) Lorsque les paramètres sont définis individuellement


@RequestMapping(path = "chunked", method = RequestMethod.POST)
public ResponseEntity<String> streamUpload(InputStream input,
        @RequestHeader(name = "Content-Type", required = false) String contentType,
        @RequestHeader(name = "Content-Length", required = false) Long contentLength,
        @RequestHeader(name = "X-SHA1-CheckSum", required = false) String checkSum,
        @RequestHeader(name = "X-FILE-NAME", required = false) String fileName) {
            
        // You should be validated 5 parameters
            
        // omitted
}

Par rapport au code ci-dessus, il utilise maintenant les fonctions de spring-mvc, comme utiliser @ RequestHeader, mais cela semble compliqué car il y a de nombreux paramètres. En outre, cette méthode ne vous permet pas d'utiliser la vérification d'entrée à l'aide de Bean Validation et BindingResult.

**(Mise en garde) Si vous spécifiez ʻInputStream` comme argument de la méthode du gestionnaire, vous pouvez recevoir les données BODY de la requête HTTP en tant que flux d'entrée. En d'autres termes, ce seront les données du fichier binaire téléchargé. ** **

2.2. Code à introduire

** Je voudrais utiliser ModelAttributeMethodProcessor pour recevoir la classe qui stocke les données requises comme argument de la méthode du gestionnaire. Il existe une interface HandlerMethodArgumentResolver comme fonction similaire, mais cela utiliseModelAttributeMethodProcessor car la vérification d'entrée n'est pas possible. ** **

StreamFile.java


public class StreamFile implements Serializable {

    private static final long serialVersionUID = 1L;

    @NotNull
    private InputStream inputStream;

    @Size(max = 200)
    @NotEmpty
    private String contentType;

    @Min(1)
    private long contentLength;

    @Size(max = 40)
    @NotEmpty
    private String checkSum;

    @Size(max = 200)
    @NotEmpty
    private String fileName;

    // omitted setter, getter
}

Comme pour la vérification d'entrée de la classe Form (bean de support de formulaire), une annotation pour la vérification d'entrée de Bean Validation est ajoutée au champ.

StreamFileModelAttributeMethodProcessor.java


package todo.app.largefile;

import java.io.IOException;
import javax.servlet.http.HttpServletRequest;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.ServletRequestParameterPropertyValues;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;

//★ Point 1
public class StreamFileModelAttributeMethodProcessor extends
                                                           ModelAttributeMethodProcessor {

    //★ Point 2
    public StreamFileModelAttributeMethodProcessor() {
        super(false);
    }

    //★ Point 3
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return StreamFile.class.equals(parameter.getParameterType());
    }

    //★ Point 4
    @Override
    protected void bindRequestParameters(WebDataBinder binder,
            NativeWebRequest request) {

        //★ Point 5
        HttpServletRequest httpRequest = request
                .getNativeRequest(HttpServletRequest.class);
        ServletRequestParameterPropertyValues pvs = new ServletRequestParameterPropertyValues(
                httpRequest);

        //★ Point 6
        pvs.add("contentType", httpRequest.getContentType());
        pvs.add("contentLength", httpRequest.getContentLengthLong());
        pvs.add("checkSum", httpRequest.getHeader("X-SHA1-CheckSum"));
        pvs.add("fileName", httpRequest.getHeader("X-FILE-NAME"));

        //★ Point 7
        try {
            pvs.add("inputStream", httpRequest.getInputStream());
        } catch (IOException e) {
            pvs.add("inputStream", null);
        }

        //★ Point 8
        binder.bind(pvs);
    }
}

** ★ Point 1 ** Définit une classe qui étend ʻorg.springframework.web.method.annotation.ModelAttributeMethodProcessor. ModelAttributeMethodProcessor est une classe qui implémente l'interface ʻorg.springframework.web.method.support.HandlerMethodArgumentResolver.

** ★ Point 2 ** [Constructeur de ModelAttributeMethodProcessor](https://docs.spring.io/autorepo/docs/spring-framework/4.3.5.RELEASE/javadoc-api/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.html########################################################################################### Appelez boolean-). Cette fois, nous allons créer un objet avec la valeur par défaut «false».

** ★ Point 3 ** Méthode supportsParameter de ʻOrg.springframework.web.method.support.HandlerMethodArgumentResolver. Traiter lorsque le type de données de l'argument de la méthode du gestionnaire est StreamFile.class`.

** ★ Point 4 ** C'est le but de cette méthode. Implémentez le processus de liaison (définition) des données obtenues à partir de la requête HTTP à l'objet argument (cette fois «StreamFile») de la méthode du gestionnaire dans la méthode «bindRequestParameters». Autrement dit, il obtient le flux d'entrée BODY de la requête HTTP et la valeur de l'en-tête de la requête HTTP et le lie (définit) à WebDataBinder.

** ★ Point 5 ** org.springframework.beans.PropertyValues Classe d'implémentation [org.springframework.web.bind.ServletRequestParameterPropertyValues](https://docs.spring.io/autorepo/docs/spring-framework/4.3.5.RELEASE/javadoc-api/org/springframework/web/ bind / ServletRequestParameterPropertyValues.html) Crée un objet de classe.

** ★ Point 6 ** Définissez la valeur obtenue à partir de l'en-tête de la requête HTTP avec la méthode ʻadd de ServletRequestParameterPropertyValues`.

** ★ Point 7 ** ★ Définissez le flux d'entrée BODY de la requête HTTP de la même manière que le point 6. J'ai décidé de mettre null lorsque ʻIOException` se produit.

** ★ Point 8 ** Liez (définissez) les PropertyValues générées au point 5 avec la méthode bind de WebDataBinder. Avec cela, la valeur de ★ points 6 et 7 sera définie dans l'objet argument de la méthode du gestionnaire (StreamFile cette fois).

FileUploadController.java


package todo.app.largefile;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("upload")
public class FileUploadController {

    /**
     * LOGGER
     */
    private static final Logger LOGGER = LoggerFactory
            .getLogger(FileUploadController.class);

    /**
     *★ Point 9
     * define max upload file size
     */
    private static final long MAX_FILE_SIZE = 500 * 1024 * 1024;

    /**
     *★ Point 9
     * buffer size 1MB
     */
    private static final int BUFFER_SIZE = 1 * 1024 * 1024;

    //★ Point 10
    @RequestMapping(path = "chunked", method = RequestMethod.POST)
    public ResponseEntity<String> streamUpload(
            @Validated StreamFile streamFile,
            BindingResult result) {

        //★ Point 11
        if (result.hasErrors()) {
            LOGGER.debug("validated error = {}", result.getAllErrors());
            return new ResponseEntity<String>("validated error!",
                    HttpStatus.BAD_REQUEST);
        }

        //★ Point 12
        if (MAX_FILE_SIZE < streamFile.getContentLength()) {
            return fileSizeOverEntity();
        }

        //★ Point 13
        try {
            File uploadFile = File.createTempFile("upload", null);
            InputStream input = streamFile.getInputStream();
            try (OutputStream output = new BufferedOutputStream(
                    new FileOutputStream(uploadFile))) {
                byte[] buffer = new byte[BUFFER_SIZE];
                long total = 0;
                int len = 0;
                while ((len = input.read(buffer)) != -1) {
                    output.write(buffer, 0, len);
                    output.flush();
                    total = total + len;
                    LOGGER.debug("writed : " + total);
                    //★ Point 12
                    if (MAX_FILE_SIZE < total) {
                        return fileSizeOverEntity();
                    }
                }
            }
            LOGGER.debug(uploadFile.getAbsolutePath());
            //★ Point 14
            return new ResponseEntity<String>("success!", HttpStatus.CREATED);
        } catch (IOException e) {
            LOGGER.error(e.getMessage());
            //★ Point 15
            return new ResponseEntity<String>("error!",
                    HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    /**
     *★ Point 12
     * @retourne ResponseEntity sur une erreur d'excès de taille de fichier
     */
    private ResponseEntity<String> fileSizeOverEntity() {
        return new ResponseEntity<String>(
                "file size is too large. " + MAX_FILE_SIZE + "(byte) or less",
                HttpStatus.BAD_REQUEST);
    }

    /**
     *Afficher l'écran du formulaire de téléchargement
     * @retour écran du formulaire de téléchargement
     */
    @RequestMapping(path = "form", method = RequestMethod.GET)
    public String form() {
        return "upload/form";
    }
}

** ★ Point 9 ** Définit la limite supérieure de la taille des données du fichier téléchargé et la taille de la mémoire tampon utilisée lors de l'enregistrement dans le fichier.

** ★ Point 10 ** Spécifiez @ Validated et BindingResult pour que la vérification d'entrée soit activée comme argument de la méthode de gestionnaire. ★ En traitant StreamFileModelAttributeMethodProcessor défini au point 1, il est possible de prendre StreamFile qui a été vérifié comme argument.

** ★ Point 11 ** Comme pour la vérification d'entrée normale, utilisez la méthode hasErrors de BindingResult pour vérifier les erreurs de vérification d'entrée. Cette fois, en cas d'erreur d'entrée, nous retournerons une réponse avec HttpStatus.BAD_REQUEST, c'est-à-dire le code d'état de réponse HTTP 400.

** ★ Point 12 ** Vérifiez si la taille des données du fichier téléchargé dépasse la limite supérieure définie au ★ Point 9. Vérifiez la taille des données à deux endroits: (1) la valeur de l'en-tête Content-Length et (2) la taille des données réellement lues. Si la limite est dépassée, nous retournerons une réponse avec HttpStatus.BAD_REQUEST, c'est-à-dire le code d'état de la réponse HTTP 400. ,

** ★ Point 13 ** Lisez les données du flux d'entrée de BODY de la requête HTTP avec la taille de tampon définie au ★ point 9, et enregistrez le fichier téléchargé en tant que fichier. Cette fois, j'ai décidé de l'enregistrer dans un répertoire temporaire. Veuillez modifier ici en fonction des besoins de votre entreprise.

** ★ Point 14 ** Étant donné que le fichier téléchargé peut être enregistré en tant que fichier sur le serveur, nous retournerons une réponse avec HttpStatus.CREATED, c'est-à-dire le code d'état de réponse HTTP 201.

(Mise en garde) Cette fois, les métadonnées du fichier téléchargé (nom de fichier, type de contenu, taille des données, somme de contrôle (valeur de hachage), etc.) ne sont pas enregistrées. Dans un système réel, lors de l'accès à un fichier téléchargé (téléchargement, ouverture avec une application, etc.), des métadonnées sont nécessaires, de sorte que les métadonnées sont enregistrées dans une base de données ou autre.

** ★ Point 15 ** Si ʻIOException se produit pendant le traitement, nous retournerons cette fois une réponse avec HttpStatus.INTERNAL_SERVER_ERROR`, c'est-à-dire le code d'état de réponse HTTP 500.

Fichier de définition Spring Bean (spring-mvc.xml)


<!-- omitted -->
<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <!-- omitted -->
        <bean class="todo.app.largefile.StreamFileModelAttributeMethodProcessor"/>
    </mvc:argument-resolvers>
</mvc:annotation-driven>

Afin d'activer le StreamFileModelAttributeMethodProcessor implémenté cette fois, ajoutez le Bean de ★ point 1 à <mvc: argument-resolvers>. Pour TERASOLUNA 5.x, définissez-le dans le fichier spring-mvc.xml.

Paramètres du serveur d'applications (par exemple, serveur Tomcat.xml)


<Connector connectionTimeout="200000" port="8090" 
    protocol="HTTP/1.1" redirectPort="8443"
    maxPostSize="-1" maxParameterCount="-1"
    maxSavePostSize="-1" maxSwallowSize="-1"
    socket.appReadBufSize="40960"/>

Si vous téléchargez un fichier volumineux, vous devez également vérifier les paramètres du serveur d'applications.

Pour Tomcat8.0, voir https://tomcat.apache.org/tomcat-8.0-doc/config/http.html.

3. Enfin

Cette fois, j'ai expliqué comment télécharger un fichier avec une grande taille de données avec TERASOLUNA 5.x (= Spring MVC). Le point est la nécessité de définir des arguments à l'aide de ModelAttributeMethodProcessor et d'ajuster le serveur d'applications en raison de la grande taille des données. ** Dans le test implémenté par le client "Comment télécharger des fichiers avec ajax sans utiliser multipart", un fichier de moins de 400 Mo est téléchargé en 6 secondes. J'ai pu le faire. ** **

Recommended Posts

Comment réaliser un téléchargement de fichiers volumineux avec TERASOLUNA 5.x (= Spring MVC)
Comment réaliser un téléchargement de fichiers volumineux avec Rest Template of Spring
J'ai essayé d'implémenter le téléchargement de fichiers avec Spring MVC
Comment réaliser le téléchargement de fichiers avec Feign
Téléchargement de fichiers avec Spring Boot
Comment tester l'écran de téléchargement de fichiers avec Spring + Selenium
Comment créer un formulaire Excel à l'aide d'un fichier modèle avec Spring MVC
Implémenter le téléchargement de fichiers avec Spring MVC
Comment charger un fichier de téléchargement Spring et afficher son contenu
Comment réaliser le téléchargement de fichiers avec Feign
[Spring MVC] Comment transmettre des variables de chemin
Comment diviser un fichier de message Spring Boot
Comment se moquer de chaque cas avec Mockito 1x
Comment utiliser MyBatis2 (iBatis) avec Spring Boot 1.4 (Spring 4)
Comment utiliser h2db intégré avec Spring Boot
Comment se lier avec un fichier de propriétés dans Spring Boot
[Spring Boot] Comment se référer au fichier de propriétés
[Java] Comment omettre l'injection de constructeur de ressort avec Lombok
Comment supprimer des tuiles d'un projet vierge dans TERASOLUNA 5.x
Comment démarrer par environnement avec Spring Boot de Maven
Téléchargement de fichiers avec Spring Boot (ne pas utiliser de fichier en plusieurs parties)
Comment modifier un projet vierge de TERASOLUNA 5.x pour prendre en charge PostgreSQL
Comment demander un fichier CSV au format JSON avec jMeter
Comment inverser la compilation du fichier apk en code source Java avec MAC
Découpez SQL en fichier de propriété avec jdbcTemplate of spring boot
[Facile] Comment formater automatiquement les fichiers Ruby erb avec vsCode
Pour recevoir une demande vide avec Spring Web MVC @RequestBody
Activer les WebJars pour les projets vierges dans TERASOLUNA 5.x (= Spring MVC)
Configuration Java avec Spring MVC
Comment numéroter (nombre) avec html.erb
Comment mettre à jour avec activerecord-import
Comment gérer l'événement où Committee :: InvalidRequest se produit en comité pendant le test de téléchargement de fichier Rspec
Introduction à Spring Boot x Open API ~ Open API créée avec le modèle d'écart de génération ~
Comment créer un fichier jar sans dépendances dans Maven
J'ai essayé de me connecter à MySQL en utilisant le modèle JDBC avec Spring MVC
Comment créer votre propre contrôleur correspondant à / error avec Spring Boot
Comment ouvrir un fichier de script à partir d'Ubuntu avec du code VS
Téléchargement automatique de fichiers avec l'ancienne gemme Ruby Que faire avec Watir
Comment utiliser Lombok au printemps
Comment faire un test unitaire de Spring AOP
Comment démarrer avec Slim
Remarques sur l'utilisation de Spring Data JDBC
Comment entourer n'importe quel caractère avec "~"
[Comment installer Spring Data Jpa]
Comment configurer Spring Boot + PostgreSQL
Comment convertir un fichier erb en haml
Comment utiliser mssql-tools avec Alpine
Comment utiliser ModelMapper (Spring boot)
[Débutant] Comment supprimer AUCUN FICHIER
Comment démarrer Camunda avec Docker
Comment appliquer immédiatement les modifications de Thymeleaf au navigateur avec #Spring Boot + maven
Comment lire le corps de la requête plusieurs fois avec Spring Boot + Spring Security
Comment réaliser une recherche hybride en utilisant l'analyse morphologique et Ngram avec Solr
Comment accéder directement à Socket avec la fonction TCP de Spring Integration
Comment créer un environnement de développement Ruby on Rails avec Docker (Rails 6.x)
Exécutable serveur avec Spring gradle Comment créer JAR et WAR