Spring RestTemplate - comment activer le débogage/journalisation complet des requêtes/réponses?

j'ai utilisé le Spring RestTemplate pendant un certain temps et j'ai toujours frappé un mur quand j'essaie de débugger ses requêtes et réponses. Je cherche essentiellement à voir les mêmes choses que je vois quand j'utilise curl avec l'option "verbose" activée. Par exemple:

curl -v http://twitter.com/statuses/public_timeline.rss

afficherait à la fois les données envoyées et les données reçues (y compris les en-têtes, cookies, etc.).

j'ai vérifié certains postes liés comme : How est-ce que j'enregistre la réponse dans la période de repos du printemps? mais je n'ai pas réussi à résoudre ce problème.

une façon de faire cela serait de réellement changer le code source RestTemplate et d'y ajouter quelques instructions de journalisation supplémentaires, mais je trouverais cette approche vraiment une chose de dernier recours. Il devrait y avoir un moyen de dire au client Web de Spring/RestTemplate de tout enregistrer d'une manière beaucoup plus conviviale.

mon but serait de pouvoir faire cela avec du code comme :

restTemplate.put("http://someurl", objectToPut, urlPathValues);

et ensuite pour obtenir le même type d'informations de débogage (que je reçois avec curl) dans le fichier journal ou dans la console. Je pense que cela serait extrêmement utile pour quiconque utilise le Spring RestTemplate et a des problèmes. Utiliser curl pour déboguer vos problèmes RestTemplate ne fonctionne tout simplement pas (dans certains cas).

131
demandé sur Community 2011-10-31 13:58:48

23 réponses

juste pour compléter l'exemple avec une mise en œuvre complète de ClientHttpRequestInterceptor pour tracer la demande et la réponse:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

    final static Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        traceRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        traceResponse(response);
        return response;
    }

    private void traceRequest(HttpRequest request, byte[] body) throws IOException {
        log.debug("===========================request begin================================================");
        log.debug("URI         : {}", request.getURI());
        log.debug("Method      : {}", request.getMethod());
        log.debug("Headers     : {}", request.getHeaders() );
        log.debug("Request body: {}", new String(body, "UTF-8"));
        log.debug("==========================request end================================================");
    }

    private void traceResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), "UTF-8"));
        String line = bufferedReader.readLine();
        while (line != null) {
            inputStringBuilder.append(line);
            inputStringBuilder.append('\n');
            line = bufferedReader.readLine();
        }
        log.debug("============================response begin==========================================");
        log.debug("Status code  : {}", response.getStatusCode());
        log.debug("Status text  : {}", response.getStatusText());
        log.debug("Headers      : {}", response.getHeaders());
        log.debug("Response body: {}", inputStringBuilder.toString());
        log.debug("=======================response end=================================================");
    }

}

puis instantiate RestTemplate en utilisant un BufferingClientHttpRequestFactory et le LoggingRequestInterceptor :

RestTemplate restTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(new LoggingRequestInterceptor());
restTemplate.setInterceptors(interceptors);

le BufferingClientHttpRequestFactory est nécessaire car nous voulons utiliser le corps de réponse à la fois dans l'intercepteur et pour le code d'appel initial. L'implémentation par défaut permet de lire le corps de réponse une seule fois.

127
répondu sofiene zaghdoudi 2018-07-12 08:28:21

extension de la réponse de @hstoerr avec un code quelconque:


créer LoggingRequestInterceptor pour enregistrer les réponses aux requêtes

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

    private static final Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

        ClientHttpResponse response = execution.execute(request, body);

        log(request,body,response);

        return response;
    }

    private void log(HttpRequest request, byte[] body, ClientHttpResponse response) throws IOException {
        //do logging
    }
}

Le Programme D'Installation RestTemplate

RestTemplate rt = new RestTemplate();

//set interceptors/requestFactory
ClientHttpRequestInterceptor ri = new LoggingRequestInterceptor();
List<ClientHttpRequestInterceptor> ris = new ArrayList<ClientHttpRequestInterceptor>();
ris.add(ri);
rt.setInterceptors(ris);
rt.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
73
répondu mjj1409 2015-02-23 13:36:31

dans la botte de printemps, vous pouvez obtenir la requête/réponse complète en définissant ceci dans les propriétés (ou autre 12 méthode de facteur)

logging.level.org.apache.http=DEBUG

ce sorties

-DEBUG .i.c.DefaultHttpClientConnectionOperator : Connecting to localhost/127.0.0.1:41827
-DEBUG .i.c.DefaultHttpClientConnectionOperator : Connection established 127.0.0.1:39546<->127.0.0.1:41827
-DEBUG o.a.http.impl.execchain.MainClientExec   : Executing request POST /v0/users HTTP/1.1
-DEBUG o.a.http.impl.execchain.MainClientExec   : Target auth state: UNCHALLENGED
-DEBUG o.a.http.impl.execchain.MainClientExec   : Proxy auth state: UNCHALLENGED
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> POST /v0/users HTTP/1.1
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Content-Type: application/json;charset=UTF-8
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Content-Length: 56
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Host: localhost:41827
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Connection: Keep-Alive
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.2 (Java/1.8.0_102)
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Accept-Encoding: gzip,deflate
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "POST /v0/users HTTP/1.1[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Content-Type: application/json;charset=UTF-8[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Content-Length: 56[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Host: localhost:41827[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.5.2 (Java/1.8.0_102)[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "{"id":null,"email":"xenoterracide@gmail.com","new":true}"

et réponse

-DEBUG .i.c.DefaultHttpClientConnectionOperator : Connecting to localhost/127.0.0.1:41827
-DEBUG .i.c.DefaultHttpClientConnectionOperator : Connection established 127.0.0.1:39546<->127.0.0.1:41827
-DEBUG o.a.http.impl.execchain.MainClientExec   : Executing request POST /v0/users HTTP/1.1
-DEBUG o.a.http.impl.execchain.MainClientExec   : Target auth state: UNCHALLENGED
-DEBUG o.a.http.impl.execchain.MainClientExec   : Proxy auth state: UNCHALLENGED
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> POST /v0/users HTTP/1.1
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Content-Type: application/json;charset=UTF-8
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Content-Length: 56
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Host: localhost:41827
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Connection: Keep-Alive
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.2 (Java/1.8.0_102)
-DEBUG org.apache.http.headers                  : http-outgoing-0 >> Accept-Encoding: gzip,deflate
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "POST /v0/users HTTP/1.1[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Content-Type: application/json;charset=UTF-8[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Content-Length: 56[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Host: localhost:41827[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.5.2 (Java/1.8.0_102)[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "Accept-Encoding: gzip,deflate[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "[\r][\n]"
-DEBUG org.apache.http.wire                     : http-outgoing-0 >> "{"id":null,"email":"xenoterracide@gmail.com","new":true}"

ou tout simplement logging.level.org.apache.http.wire=DEBUG qui semble contenir tous les renseignements pertinents

62
répondu xenoterracide 2016-08-23 20:11:42

j'ai finalement trouvé un moyen de le faire de la bonne manière. La plupart de la solution vient de comment configurer Spring et SLF4J pour que je puisse obtenir la journalisation?

il semble qu'il y ait deux choses à faire:

  1. ajouter la ligne suivante dans log4j.propriétés: log4j.logger.httpclient.wire=DEBUG
  2. assurez-vous que le printemps n'ignore pas votre configuration de journalisation

la seconde problème arrive surtout au printemps environnements où slf4j est utilisé (comme c'était mon cas). Par conséquent, lorsque slf4j est utilisé, s'assurer que les deux choses suivantes se produisent:

  1. il n'y a pas de bibliothèque commune-logging dans votre classepath : ceci peut être fait en ajoutant les descripteurs d'exclusion dans votre pom:

            <exclusions><exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
    
  2. the log4j.le fichier properties est stocké quelque part dans classpath où spring peut le trouver/le voir. Si vous avez des problèmes avec cela, une solution de dernier recours serait de mettre le log4j.propriétés fichier dans le paquet par défaut (pas une bonne pratique, mais juste pour voir que les choses fonctionnent comme vous vous y attendez)

24
répondu Paul Sabou 2017-05-23 12:26:07

Aucune de ces réponses résoudre 100% du problème. mjj1409 obtient le plus, mais idéalement évite la question de l'enregistrement de la réponse, qui prend un peu plus de travail. Paul Sabou fournit une solution qui semble réaliste, mais qui ne fournit pas assez de détails pour réellement mettre en œuvre (et cela n'a pas fonctionné du tout pour moi). Sofiene a obtenu la journalisation mais avec un problème critique: la réponse n'est plus lisible car le flux d'entrée a déjà été consommé!

Je recommande l'utilisation D'un BufferingClientHttpResponseWrapper pour envelopper l'objet de réponse pour permettre la lecture du corps de réponse plusieurs fois:

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        ClientHttpResponse response = execution.execute(request, body);

        response = log(request, body, response);

        return response;
    }

    private ClientHttpResponse log(final HttpRequest request, final byte[] body, final ClientHttpResponse response) {
        final ClientHttpResponse responseCopy = new BufferingClientHttpResponseWrapper(response);
        logger.debug("Method: ", request.getMethod().toString());
        logger.debug("URI: ", , request.getURI().toString());
        logger.debug("Request Body: " + new String(body));
        logger.debug("Response body: " + IOUtils.toString(responseCopy.getBody()));
        return responseCopy;
    }

}

cela ne consommera pas le flux entrant parce que le corps de réponse est chargé dans la mémoire et peut être lu plusieurs fois. Si vous n'avez pas le BufferingClientHttpResponseWrapper sur votre chemin de classe, vous pouvez trouver l'implémentation simple ici:

https://github.com/spring-projects/spring-android/blob/master/spring-android-rest-template/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java

pour la mise en place du RestTemplate:

LoggingRequestInterceptor loggingInterceptor = new LoggingRequestInterceptor();
restTemplate.getInterceptors().add(loggingInterceptor);
23
répondu James Watkins 2016-09-24 13:16:07

la solution donnée par La xenoterracide à utiliser

logging.level.org.apache.http=DEBUG

est bon mais le problème est que par défaut Apache HttpComponents n'est pas utilisé.

pour utiliser Apache HttpComponents ajouter à votre pom.xml

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpasyncclient</artifactId>
</dependency>

et configurer RestTemplate avec:

RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory());
22
répondu Ortomala Lokni 2017-02-01 15:38:51

votre meilleur pari est d'ajouter logging.level.org.springframework.web.client.RestTemplate=DEBUG au fichier application.properties .

D'autres solutions comme log4j.logger.httpclient.wire ne fonctionneront pas toujours parce qu'elles supposent que vous utilisez log4j et Apache HttpClient , ce qui n'est pas toujours vrai.

notez cependant que cette syntaxe ne fonctionnera que sur les dernières versions de la botte de printemps.

17
répondu gamliela 2016-07-04 13:34:36

RestTemplate D'Exploitation Forestière

Option 1. Ouvrir la journalisation de débogage.

Configurate RestTemplate

  • par défaut le RestTemplate s'appuie sur les installations JDK standard pour établir des connexions HTTP. Vous pouvez passer à une autre bibliothèque HTTP comme Apache HttpComponents

    @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { RestTemplate restTemplate = builder.construire(); return restTemplate; }

Configurer la journalisation

  • application.yml

    journalisation: niveau: org.springframework.Web.client.RestTemplate: DEBUG

l'Option 2. Utilisant L'Intercepteur

Wrapper Response

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;

public final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {

    private final ClientHttpResponse response;

    private byte[] body;


    BufferingClientHttpResponseWrapper(ClientHttpResponse response) {
        this.response = response;
    }

    public HttpStatus getStatusCode() throws IOException {
        return this.response.getStatusCode();
    }

    public int getRawStatusCode() throws IOException {
        return this.response.getRawStatusCode();
    }

    public String getStatusText() throws IOException {
        return this.response.getStatusText();
    }

    public HttpHeaders getHeaders() {
        return this.response.getHeaders();
    }

    public InputStream getBody() throws IOException {
        if (this.body == null) {
            this.body = StreamUtils.copyToByteArray(this.response.getBody());
        }
        return new ByteArrayInputStream(this.body);
    }

    public void close() {
        this.response.close();
    }
}

Mettre En Œuvre L'Intercepteur

package com.example.logging;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class LoggingRestTemplate implements ClientHttpRequestInterceptor {

    private final static Logger LOGGER = LoggerFactory.getLogger(LoggingRestTemplate.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
            ClientHttpRequestExecution execution) throws IOException {
        traceRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        return traceResponse(response);
    }

    private void traceRequest(HttpRequest request, byte[] body) throws IOException {
        if (!LOGGER.isDebugEnabled()) {
            return;
        }
        LOGGER.debug(
                "==========================request begin==============================================");
        LOGGER.debug("URI                 : {}", request.getURI());
        LOGGER.debug("Method            : {}", request.getMethod());
        LOGGER.debug("Headers         : {}", request.getHeaders());
        LOGGER.debug("Request body: {}", new String(body, "UTF-8"));
        LOGGER.debug(
                "==========================request end================================================");
    }

    private ClientHttpResponse traceResponse(ClientHttpResponse response) throws IOException {
        if (!LOGGER.isDebugEnabled()) {
            return response;
        }
        final ClientHttpResponse responseWrapper = new BufferingClientHttpResponseWrapper(response);
        StringBuilder inputStringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(responseWrapper.getBody(), "UTF-8"));
        String line = bufferedReader.readLine();
        while (line != null) {
            inputStringBuilder.append(line);
            inputStringBuilder.append('\n');
            line = bufferedReader.readLine();
        }
        LOGGER.debug(
                "==========================response begin=============================================");
        LOGGER.debug("Status code    : {}", responseWrapper.getStatusCode());
        LOGGER.debug("Status text    : {}", responseWrapper.getStatusText());
        LOGGER.debug("Headers            : {}", responseWrapper.getHeaders());
        LOGGER.debug("Response body: {}", inputStringBuilder.toString());
        LOGGER.debug(
                "==========================response end===============================================");
        return responseWrapper;
    }

}

Configurate RestTemplate

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    RestTemplate restTemplate = builder.build();
    restTemplate.setInterceptors(Collections.singletonList(new LoggingRestTemplate()));
    return restTemplate;
}

Configurer la journalisation

  • Vérifier le colis de LoggingRestTemplate, par exemple dans application.yml :

    journalisation: niveau: COM.exemple.logging: DEBUG

l'Option 3. En utilisant le composant httpcomponent

importation httpcomponent dependency

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpasyncclient</artifactId>

Configurate RestTemplate

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    RestTemplate restTemplate = builder.build();
    restTemplate.setRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory());
    return restTemplate;
}

Configurer la journalisation

  • Vérifier le colis de LoggingRestTemplate, par exemple dans application.yml :

    journalisation: niveau: org.Apache.http: DEBUG

11
répondu user2746033 2017-11-24 06:34:11

outre la journalisation HttpClient décrite dans l'autre réponse , vous pouvez également introduire un ClientHttpRequestInterceptor qui lit le corps de la requête et de la réponse et l'enregistre. Vous pouvez faire cela si D'autres choses utilisent aussi le HttpClient, ou si vous voulez un format de journalisation personnalisé. Attention: vous voudrez donner au RestTemplate un BufferingClientHttpRequestFactory de sorte que vous puissiez lire la réponse deux fois.

10
répondu Hans-Peter Störr 2017-05-23 12:10:11

en supposant que RestTemplate est configuré pour utiliser HttpClient 4.x, vous pouvez lire sur la documentation de journalisation de HttpClient ici . Les bûcherons sont différents de ceux spécifiés dans les autres réponses.

la configuration de journalisation pour HttpClient 3.x est disponible ici .

6
répondu Emerson Farrugia 2014-11-13 15:25:18

Vous pouvez utiliser printemps-repos-modèle-logger journal RestTemplate le trafic HTTP.

ajouter une dépendance à votre projet Maven:

<dependency>
    <groupId>org.hobsoft.spring</groupId>
    <artifactId>spring-rest-template-logger</artifactId>
    <version>2.0.0</version>
</dependency>

puis personnalisez votre RestTemplate comme suit:

RestTemplate restTemplate = new RestTemplateBuilder()
    .customizers(new LoggingCustomizer())
    .build()

maintenant tout le trafic HTTP RestTemplate sera journalisé en org.hobsoft.spring.resttemplatelogger.LoggingCustomizer au niveau de débogage.

avertissement: j'ai écrit cette bibliothèque.

5
répondu Mark Hobson 2018-09-09 16:01:24

le truc de configurer votre RestTemplate avec un BufferingClientHttpRequestFactory ne fonctionne pas si vous utilisez un ClientHttpRequestInterceptor , ce que vous ferez si vous essayez de vous connecter via des intercepteurs. Cela est dû à la façon dont InterceptingHttpAccessor (qui RestTemplate sous-classes) fonctionne.

pour faire court... il suffit d'utiliser cette classe à la place de RestTemplate (notez qu'elle utilise L'API de journalisation SLF4J, éditez au besoin):

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestTemplate;

/**
 * A {@link RestTemplate} that logs every request and response.
 */
public class LoggingRestTemplate extends RestTemplate {

    // Bleh, this class is not public
    private static final String RESPONSE_WRAPPER_CLASS = "org.springframework.http.client.BufferingClientHttpResponseWrapper";

    private Logger log = LoggerFactory.getLogger(this.getClass());

    private boolean hideAuthorizationHeaders = true;
    private Class<?> wrapperClass;
    private Constructor<?> wrapperConstructor;

    /**
     * Configure the logger to log requests and responses to.
     *
     * @param log log destination, or null to disable
     */
    public void setLogger(Logger log) {
        this.log = log;
    }

    /**
     * Configure the logger to log requests and responses to by name.
     *
     * @param name name of the log destination, or null to disable
     */
    public void setLoggerName(String name) {
        this.setLogger(name != null ? LoggerFactory.getLogger(name) : null);
    }

    /**
     * Configure whether to hide the contents of {@code Authorization} headers.
     *
     * <p>
     * Default true.
     *
     * @param hideAuthorizationHeaders true to hide, otherwise false
     */
    public void setHideAuthorizationHeaders(boolean hideAuthorizationHeaders) {
        this.hideAuthorizationHeaders = hideAuthorizationHeaders;
    }

    /**
     * Log a request.
     */
    protected void traceRequest(HttpRequest request, byte[] body) {
        this.log.debug("xmit: {} {}\n{}{}", request.getMethod(), request.getURI(), this.toString(request.getHeaders()),
          body != null && body.length > 0 ? "\n\n" + new String(body, StandardCharsets.UTF_8) : "");
    }

    /**
     * Log a response.
     */
    protected void traceResponse(ClientHttpResponse response) {
        final ByteArrayOutputStream bodyBuf = new ByteArrayOutputStream();
        HttpStatus statusCode = null;
        try {
            statusCode = response.getStatusCode();
        } catch (IOException e) {
            // ignore
        }
        String statusText = null;
        try {
            statusText = response.getStatusText();
        } catch (IOException e) {
            // ignore
        }
        try (final InputStream input = response.getBody()) {
            byte[] b = new byte[1024];
            int r;
            while ((r = input.read(b)) != -1)
                bodyBuf.write(b, 0, r);
        } catch (IOException e) {
            // ignore
        }
        this.log.debug("recv: {} {}\n{}{}", statusCode, statusText, this.toString(response.getHeaders()),
          bodyBuf.size() > 0 ? "\n\n" + new String(bodyBuf.toByteArray(), StandardCharsets.UTF_8) : "");
    }

    @PostConstruct
    private void addLoggingInterceptor() {
        this.getInterceptors().add(new ClientHttpRequestInterceptor() {
            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
              throws IOException {

                // Log request
                if (LoggingRestTemplate.this.log != null && LoggingRestTemplate.this.log.isDebugEnabled())
                    LoggingRestTemplate.this.traceRequest(request, body);

                // Perform request
                ClientHttpResponse response = execution.execute(request, body);

                // Log response
                if (LoggingRestTemplate.this.log != null && LoggingRestTemplate.this.log.isDebugEnabled()) {
                    final ClientHttpResponse bufferedResponse = LoggingRestTemplate.this.ensureBuffered(response);
                    if (bufferedResponse != null) {
                        LoggingRestTemplate.this.traceResponse(bufferedResponse);
                        response = bufferedResponse;
                    }
                }

                // Done
                return response;
            }
        });
    }

    private ClientHttpResponse ensureBuffered(ClientHttpResponse response) {
        try {
            if (this.wrapperClass == null)
                this.wrapperClass = Class.forName(RESPONSE_WRAPPER_CLASS, false, ClientHttpResponse.class.getClassLoader());
            if (!this.wrapperClass.isInstance(response)) {
                if (this.wrapperConstructor == null) {
                    this.wrapperConstructor = this.wrapperClass.getDeclaredConstructor(ClientHttpResponse.class);
                    this.wrapperConstructor.setAccessible(true);
                }
                response = (ClientHttpResponse)this.wrapperConstructor.newInstance(response);
            }
            return response;
        } catch (Exception e) {
            this.log.error("error creating {} instance: {}", RESPONSE_WRAPPER_CLASS, e);
            return null;
        }
    }

    private String toString(HttpHeaders headers) {
        final StringBuilder headerBuf = new StringBuilder();
        for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
            if (headerBuf.length() > 0)
                headerBuf.append('\n');
            final String name = entry.getKey();
            for (String value : entry.getValue()) {
                if (this.hideAuthorizationHeaders && name.equalsIgnoreCase(HttpHeaders.AUTHORIZATION))
                    value = "[omitted]";
                headerBuf.append(name).append(": ").append(value);
            }
        }
        return headerBuf.toString();
    }
}

je suis d'accord c'est idiot, qu'il prend ce beaucoup de travail juste pour ce faire.

2
répondu Archie 2016-04-05 22:29:04

ajouter à la discussion ci-dessus cela ne représente que des scénarios heureux. vous ne pourrez probablement pas enregistrer la réponse si une "erreur 151920920 arrive .

dans ce cas plus tous les cas ci-dessus, vous devez outrepasser DefaultResponseErrorHandler et le mettre comme ci-dessous

restTemplate.setErrorHandler(new DefaultResponseErrorHandlerImpl());
2
répondu user666 2016-04-07 13:43:28

étrangement, aucune de ces solutions ne fonctionne car RestTemplate ne semble pas renvoyer la réponse sur quelques erreurs client et serveur 500x. Dans ce cas, vous aurez log those ainsi en mettant en œuvre ResponseErrorHandler comme suit. Voici un projet de code, mais vous obtenez le point:

vous pouvez définir le même intercepteur que le gestionnaire d'erreurs:

restTemplate.getInterceptors().add(interceptor);
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
restTemplate.setErrorHandler(interceptor);

et l'intercept implémente les deux interfaces:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus.Series;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.ResponseErrorHandler;

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor, ResponseErrorHandler {
    static final Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);
    static final DefaultResponseErrorHandler defaultResponseErrorHandler = new DefaultResponseErrorHandler();
    final Set<Series> loggableStatuses = new HashSet();

    public LoggingRequestInterceptor() {
    }

    public LoggingRequestInterceptor(Set<Series> loggableStatuses) {
        loggableStatuses.addAll(loggableStatuses);
    }

    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        this.traceRequest(request, body);
        ClientHttpResponse response = execution.execute(request, body);
        if(response != null) {
            this.traceResponse(response);
        }

        return response;
    }

    private void traceRequest(HttpRequest request, byte[] body) throws IOException {
        log.debug("===========================request begin================================================");
        log.debug("URI         : {}", request.getURI());
        log.debug("Method      : {}", request.getMethod());
        log.debug("Headers     : {}", request.getHeaders());
        log.debug("Request body: {}", new String(body, "UTF-8"));
        log.debug("==========================request end================================================");
    }

    private void traceResponse(ClientHttpResponse response) throws IOException {
        if(this.loggableStatuses.isEmpty() || this.loggableStatuses.contains(response.getStatusCode().series())) {
            StringBuilder inputStringBuilder = new StringBuilder();

            try {
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), "UTF-8"));

                for(String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine()) {
                    inputStringBuilder.append(line);
                    inputStringBuilder.append('\n');
                }
            } catch (Throwable var5) {
                log.error("cannot read response due to error", var5);
            }

            log.debug("============================response begin==========================================");
            log.debug("Status code  : {}", response.getStatusCode());
            log.debug("Status text  : {}", response.getStatusText());
            log.debug("Headers      : {}", response.getHeaders());
            log.debug("Response body: {}", inputStringBuilder.toString());
            log.debug("=======================response end=================================================");
        }

    }

    public boolean hasError(ClientHttpResponse response) throws IOException {
        return defaultResponseErrorHandler.hasError(response);
    }

    public void handleError(ClientHttpResponse response) throws IOException {
        this.traceResponse(response);
        defaultResponseErrorHandler.handleError(response);
    }
}
2
répondu kisna 2017-10-18 02:11:20

ce n'est peut-être pas la bonne façon de le faire, mais je pense que c'est l'approche la plus simple pour imprimer des requêtes et des réponses sans trop remplir les journaux.

en ajoutant ci-dessous 2 lignes application.properties enregistre toutes les requêtes et les réponses à la première ligne pour enregistrer les requêtes et à la deuxième ligne pour enregistrer les réponses.

logging.level.org.springframework.web.client.RestTemplate=DEBUG logging.level.org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor=DEBUG

2
répondu User9123 2018-05-29 15:37:29

meilleure solution maintenant, il suffit d'ajouter la dépendance:

<dependency>
  <groupId>com.github.zg2pro</groupId>
  <artifactId>spring-rest-basis</artifactId>
  <version>v.x</version>
</dependency>

il contient une classe Loggreequestinterceptor que vous pouvez ajouter à votre RestTemplate:

intègre cet utilitaire en l'ajoutant en tant qu'intercepteur à un temps de Restlate de printemps, de la manière suivante:

restTemplate.setRequestFactory(LoggingRequestFactoryFactory.build());

et ajouter une implémentation slf4j à votre framework comme log4j.

ou utiliser directement "Zg2proRestTemplate " . La" meilleure réponse " de @PaulSabou semble tellement, depuis httpclient et tous apache.les libs http ne sont pas nécessairement chargés lors de l'utilisation d'un RestTemplate de printemps.

0
répondu Moses Meyer 2017-08-24 13:12:50

a voulu ajouter ma mise en œuvre de ceci aussi bien. Je m'excuse pour tous les semi-colons disparus, c'est écrit dans Groovy.

j'avais besoin de quelque chose de plus configurable que la réponse acceptée fournie. Voici un bean rest template qui est très agile et enregistrera tout comme L'OP cherche.

Custom Logging Intercepteur De La Classe:

import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.util.StreamUtils

import java.nio.charset.Charset

class HttpLoggingInterceptor implements ClientHttpRequestInterceptor {

    private final static Logger log = LoggerFactory.getLogger(HttpLoggingInterceptor.class)

    @Override
    ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        logRequest(request, body)
        ClientHttpResponse response = execution.execute(request, body)
        logResponse(response)
        return response
    }

    private void logRequest(HttpRequest request, byte[] body) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("===========================request begin================================================")
            log.debug("URI         : {}", request.getURI())
            log.debug("Method      : {}", request.getMethod())
            log.debug("Headers     : {}", request.getHeaders())
            log.debug("Request body: {}", new String(body, "UTF-8"))
            log.debug("==========================request end================================================")
        }
    }

    private void logResponse(ClientHttpResponse response) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("============================response begin==========================================")
            log.debug("Status code  : {}", response.getStatusCode())
            log.debug("Status text  : {}", response.getStatusText())
            log.debug("Headers      : {}", response.getHeaders())
            log.debug("Response body: {}", StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()))
            log.debug("=======================response end=================================================")
        }
    }
}

Rest Template Bean Définition:

@Bean(name = 'myRestTemplate')
RestTemplate myRestTemplate(RestTemplateBuilder builder) {

    RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(10 * 1000) // 10 seconds
            .setSocketTimeout(300 * 1000) // 300 seconds
            .build()

    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager()
    connectionManager.setMaxTotal(10)
    connectionManager.closeIdleConnections(5, TimeUnit.MINUTES)

    CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .setDefaultRequestConfig(requestConfig)
            .disableRedirectHandling()
            .build()

    RestTemplate restTemplate = builder
            .rootUri("https://domain.server.com")
            .basicAuthorization("username", "password")
            .requestFactory(new BufferingClientHttpRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)))
            .interceptors(new HttpLoggingInterceptor())
            .build()

    return restTemplate
}

mise en Œuvre:

@Component
class RestService {

    private final RestTemplate restTemplate
    private final static Logger log = LoggerFactory.getLogger(RestService.class)

    @Autowired
    RestService(
            @Qualifier("myRestTemplate") RestTemplate restTemplate
    ) {
        this.restTemplate = restTemplate
    }

    // add specific methods to your service that access the GET and PUT methods

    private <T> T getForObject(String path, Class<T> object, Map<String, ?> params = [:]) {
        try {
            return restTemplate.getForObject(path, object, params)
        } catch (HttpClientErrorException e) {
            log.warn("Client Error (${path}): ${e.responseBodyAsString}")
        } catch (HttpServerErrorException e) {
            String msg = "Server Error (${path}): ${e.responseBodyAsString}"
            log.error(msg, e)
        } catch (RestClientException e) {
            String msg = "Error (${path})"
            log.error(msg, e)
        }
        return null
    }

    private <T> T putForObject(String path, T object) {
        try {
            HttpEntity<T> request = new HttpEntity<>(object)
            HttpEntity<T> response = restTemplate.exchange(path, HttpMethod.PUT, request, T)
            return response.getBody()
        } catch (HttpClientErrorException e) {
            log.warn("Error (${path}): ${e.responseBodyAsString}")
        } catch (HttpServerErrorException e) {
            String msg = "Error (${path}): ${e.responseBodyAsString}"
            log.error(msg, e)
        } catch (RestClientException e) {
            String msg = "Error (${path})"
            log.error(msg, e)
        }
        return null
    }
}
0
répondu Jason Slobotski 2018-04-20 18:24:45

reportez-vous à la Q/A pour l'enregistrement de la demande et de la réponse pour le reste modèle en permettant de multiples lectures sur la HttpInputStream

Pourquoi mon client ClientHttpRequestInterceptor avec réponse vide

0
répondu maya16 2018-04-27 03:21:07

comme @MilacH l'a souligné, il y a une erreur dans la mise en œuvre. Si un statusCode > 400 est retourné, une IOException est lancée, car le errorHandler n'est pas invoqué, à partir des intercepteurs. L'exception peut être ignorée et se retrouve alors dans la méthode handler.

package net.sprd.fulfillment.common;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import static java.nio.charset.StandardCharsets.UTF_8;

public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

    final static Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class);

    @SuppressWarnings("HardcodedLineSeparator")
    public static final char LINE_BREAK = '\n';

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        try {
            traceRequest(request, body);
        } catch (Exception e) {
            log.warn("Exception in LoggingRequestInterceptor while tracing request", e);
        }

        ClientHttpResponse response = execution.execute(request, body);

        try {
            traceResponse(response);
        } catch (IOException e) {
            // ignore the exception here, as it will be handled by the error handler of the restTemplate
            log.warn("Exception in LoggingRequestInterceptor", e);
        }
        return response;
    }

    private void traceRequest(HttpRequest request, byte[] body) {
        log.info("===========================request begin================================================");
        log.info("URI         : {}", request.getURI());
        log.info("Method      : {}", request.getMethod());
        log.info("Headers     : {}", request.getHeaders());
        log.info("Request body: {}", new String(body, UTF_8));
        log.info("==========================request end================================================");
    }

    private void traceResponse(ClientHttpResponse response) throws IOException {
        StringBuilder inputStringBuilder = new StringBuilder();
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getBody(), UTF_8))) {
            String line = bufferedReader.readLine();
            while (line != null) {
                inputStringBuilder.append(line);
                inputStringBuilder.append(LINE_BREAK);
                line = bufferedReader.readLine();
            }
        }

        log.info("============================response begin==========================================");
        log.info("Status code  : {}", response.getStatusCode());
        log.info("Status text  : {}", response.getStatusText());
        log.info("Headers      : {}", response.getHeaders());
        log.info("Response body: {}", inputStringBuilder);
        log.info("=======================response end=================================================");
    }

}
0
répondu Tony Findeisen 2018-09-14 11:03:09

comme indiqué dans les autres réponses, l'organisme de réponse a besoin d'un traitement spécial afin qu'il puisse être lu de façon répétée (par défaut, son contenu est consommé lors de la première lecture).

au lieu d'utiliser le BufferingClientHttpRequestFactory lors de la configuration de la requête, l'intercepteur lui-même peut envelopper la réponse et s'assurer que le contenu est conservé et peut être lu à plusieurs reprises (par l'enregistreur ainsi que par le consommateur de la réponse):

My interceptor, qui

  • tampons le corps de la réponse à l'aide d'un wrapper
  • les journaux dans une de façon plus compacte
  • enregistre le code d'état identifiant ainsi (par exemple 201 créé)
  • comprend un numéro de séquence de requête permettant de distinguer facilement les entrées de journal simultanées de fils multiples

Code:

public class LoggingInterceptor implements ClientHttpRequestInterceptor {

    private final Logger log = LoggerFactory.getLogger(getClass());
    private AtomicInteger requestNumberSequence = new AtomicInteger(0);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        int requestNumber = requestNumberSequence.incrementAndGet();
        logRequest(requestNumber, request, body);
        ClientHttpResponse response = execution.execute(request, body);
        response = new BufferedClientHttpResponse(response);
        logResponse(requestNumber, response);
        return response;
    }

    private void logRequest(int requestNumber, HttpRequest request, byte[] body) {
        if (log.isDebugEnabled()) {
            String prefix = requestNumber + " > ";
            log.debug("{} Request: {} {}", prefix, request.getMethod(), request.getURI());
            log.debug("{} Headers: {}", prefix, request.getHeaders());
            if (body.length > 0) {
                log.debug("{} Body: \n{}", prefix, new String(body, StandardCharsets.UTF_8));
            }
        }
    }

    private void logResponse(int requestNumber, ClientHttpResponse response) throws IOException {
        if (log.isDebugEnabled()) {
            String prefix = requestNumber + " < ";
            log.debug("{} Response: {} {} {}", prefix, response.getStatusCode(), response.getStatusCode().name(), response.getStatusText());
            log.debug("{} Headers: {}", prefix, response.getHeaders());
            String body = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
            if (body.length() > 0) {
                log.debug("{} Body: \n{}", prefix, body);
            }
        }
    }

    /**
     * Wrapper around ClientHttpResponse, buffers the body so it can be read repeatedly (for logging & consuming the result).
     */
    private static class BufferedClientHttpResponse implements ClientHttpResponse {

        private final ClientHttpResponse response;
        private byte[] body;

        public BufferedClientHttpResponse(ClientHttpResponse response) {
            this.response = response;
        }

        @Override
        public HttpStatus getStatusCode() throws IOException {
            return response.getStatusCode();
        }

        @Override
        public int getRawStatusCode() throws IOException {
            return response.getRawStatusCode();
        }

        @Override
        public String getStatusText() throws IOException {
            return response.getStatusText();
        }

        @Override
        public void close() {
            response.close();
        }

        @Override
        public InputStream getBody() throws IOException {
            if (body == null) {
                body = StreamUtils.copyToByteArray(response.getBody());
            }
            return new ByteArrayInputStream(body);
        }

        @Override
        public HttpHeaders getHeaders() {
            return response.getHeaders();
        }
    }
}

Configuration:

 @Bean
    public RestTemplateBuilder restTemplateBuilder() {
        return new RestTemplateBuilder()
                .additionalInterceptors(Collections.singletonList(new LoggingInterceptor()));
    }

exemple de sortie log:

2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 >  Request: POST http://localhost:53969/payment/v4/private/payment-lists/10022/templates
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 >  Headers: {Accept=[application/json, application/*+json], Content-Type=[application/json;charset=UTF-8], Content-Length=[986]}
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 >  Body: 
{"idKey":null, ...}
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 <  Response: 200 OK 
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 <  Headers: {Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Mon, 08 Oct 2018 08:58:53 GMT]}
2018-10-08 10:58:53 [main] DEBUG x.y.z.LoggingInterceptor - 2 <  Body: 
{ "idKey" : "10022", ...  }
0
répondu Peter Walser 2018-10-08 09:01:11

tant de réponses ici nécessitent des changements de codage et des classes personnalisées et il n'est vraiment pas nécessaire. Gte un proxy de débogage tel que fiddler et définissez votre environnement java pour utiliser le proxy sur la ligne de commande (-Dhttp.proxyHost et-Dhttp.proxyPort) puis lancer fiddler et vous pouvez voir les requêtes et les réponses dans leur intégralité. Vient aussi avec beaucoup d'avantages accessoires tels que la capacité de tinker avec les résultats et les réponses avant et après qu'ils sont envoyés pour exécuter des expériences avant de s'engager dans la modification du serveur.

la dernière partie d'un problème qui peut surgir est que si vous devez utiliser HTTPS, vous devrez exporter le certificat SSL de fiddler et l'importer dans le Java keystore (cacerts) Conseil: le mot de passe java keystore par défaut est habituellement"changeit".

0
répondu Lee Burch 2018-10-08 19:12:09

relié à la réponse en utilisant ClientHttpInterceptor, j'ai trouvé un moyen de garder toute la réponse sans usines de Tamponnage. Il suffit de stocker le flux d'entrée de corps de réponse à l'intérieur du tableau byte en utilisant une méthode utils qui va copier ce tableau de corps, mais important, entourer cette méthode avec try catch parce qu'il va casser si la réponse est vide (qui est la cause de L'Exception D'accès aux ressources) et dans catch juste créer le tableau byte vide, et que juste créer la classe interne anonyme de ClientHttpResponse utilisant ce tableau et d'autres paramètres de la réponse originale. Que vous pouvez retourner ce nouvel objet ClientHttpResponse à la chaîne d'exécution de template rest et vous pouvez enregistrer la réponse en utilisant le tableau body byte qui est précédemment stocké. De cette façon, vous éviterez de consommer InputStream dans la réponse réelle et vous pouvez utiliser Rest Template réponse comme il est. Remarque, cela peut être dangereux si votre réponse est trop gros

-1
répondu NenadTzar 2018-03-02 13:42:08

mon enregistreur de config xml utilisé

<logger name="org.springframework.web.client.RestTemplate">
    <level value="trace"/>
</logger>

, alors vous obtiendrez quelque chose comme ci-dessous:

DEBUG org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:92) : Reading [com.test.java.MyClass] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@604525f1]

via HttpMessageConverterExtractor.java:92,vous devez continuer à déboguer,et dans mon cas,j'ai obtenu ceci:

genericMessageConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);

et ceci:

outputMessage.getBody().flush();

outputMessage.getBody () contient le message http (type de poste) envoie

-2
répondu danshijin 2017-01-19 02:36:56