我终于找到了一个正确的方法来做这件事。 大部分的解决方案来自 我如何配置Spring和SLF4J以获得日志记录? < / p >

看来有两件事需要做:

  1. 在log4j中添加以下行。属性:log4j.logger.httpclient.wire=DEBUG
  2. 确保spring不会忽略您的日志配置
第二个问题主要发生在使用slf4j的spring环境中(就像我的情况一样)。 因此,当使用slf4j时,请确保发生以下两件事:

  1. 在你的类路径中没有公共日志库:这可以通过在pom中添加排除描述符来实现:

            <exclusions><exclusion>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    </exclusion>
    </exclusions>
    
  2. The log4j.properties file is stored somewhere in the classpath where spring can find/see it. If you have problems with this, a last resort solution would be to put the log4j.properties file in the default package (not a good practice but just to see that things work as you expect)

除了在另一个答案中描述的HttpClient日志之外,你还可以引入一个ClientHttpRequestInterceptor,它读取请求体和响应并记录它。如果其他东西也使用HttpClient,或者你想要一个自定义的日志格式,你可能需要这样做。注意:您需要给RestTemplate一个BufferingClientHttpRequestFactory,这样您就可以读取两次响应。

用一些代码扩展@hstoerr answer:


创建LoggingRequestInterceptor来记录请求响应

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
}
}

设置创建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());

假设RestTemplate配置来使用HttpClient 4。x,你可以阅读HttpClient的日志文档在这里。这些记录器与其他答案中指定的记录器不同。

HttpClient 3的日志配置。在这里. x可用。

ClientHttpRequestInterceptor的完整实现来完成这个示例,以跟踪请求和响应:

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.info("===========================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.info("==========================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.info("============================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.info("=======================response end=================================================");
}


}

然后使用BufferingClientHttpRequestFactoryLoggingRequestInterceptor实例化RestTemplate:

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

BufferingClientHttpRequestFactory是必需的,因为我们想在拦截器和初始调用代码中使用响应体。默认实现只允许读取响应体一次。

这些答案都不能完全解决问题。Mjj1409实现了大部分功能,但方便地避免了记录响应的问题,因为这需要更多的工作。Paul Sabou提供了一个看似现实的解决方案,但没有提供足够的细节来实际实现(对我来说根本没用)。Sofiene得到了日志记录,但遇到了一个严重的问题:响应不再可读,因为输入流已经被消耗了!

我建议使用BufferingClientHttpResponseWrapper来包装响应对象,以允许多次读取响应体:

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;
}


}

这将不会消耗InputStream,因为响应体被加载到内存中,可以多次读取。如果你的类路径上没有BufferingClientHttpResponseWrapper,你可以在这里找到简单的实现:

https://github.com/spring-projects/spring-android/blob/master/spring-android-rest-template/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java < a href = " https://github.com/spring-projects/spring-android/blob/master/spring-android-rest-template/src/main/java/org/springframework/http/client/BufferingClientHttpResponseWrapper.java " > < / >

设置RestTemplate:

LoggingRequestInterceptor loggingInterceptor = new LoggingRequestInterceptor();
restTemplate.getInterceptors().add(loggingInterceptor);

如果你正在使用任何ClientHttpRequestInterceptor,那么用BufferingClientHttpRequestFactory配置你的RestTemplate的技巧就不起作用了,如果你试图通过拦截器记录日志,你就会这样做。这是由于InterceptingHttpAccessor (RestTemplate的子类)的工作方式。

长话短说……只需要使用这个类来代替RestTemplate(注意这使用SLF4J日志API,根据需要编辑):

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();
}
}

我同意花这么多功夫来做这件事很愚蠢。

除了上面的讨论,这只代表了快乐的场景。如果错误来了,你可能无法记录响应。

在这种情况下,加上上面所有的情况,你必须覆盖DefaultResponseErrorHandler,并像下面那样设置它

restTemplate.setErrorHandler(new DefaultResponseErrorHandlerImpl());

最好的方法是将logging.level.org.springframework.web.client.RestTemplate=DEBUG添加到application.properties文件中。

其他解决方案,如设置log4j.logger.httpclient.wire并不总是有效,因为它们假设你使用log4j和Apache HttpClient,这并不总是正确的。

但是请注意,此语法仅适用于最新版本的Spring Boot。

在Spring Boot中,您可以通过在属性中设置这个(或其他12因素方法)来获得完整的请求/响应。

logging.level.org.apache.http=DEBUG

这个输出

-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}"

和响应

-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}"

或者只是logging.level.org.apache.http.wire=DEBUG,它似乎包含了所有相关的信息

我的日志记录器配置使用XML

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

然后你会得到如下内容:

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]

通过HttpMessageConverterExtractor.java:92,你需要继续调试,在我的情况下,我得到了这个:

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

这:

outputMessage.getBody().flush();

outputMessage.getBody()包含http(post类型)发送的消息

所给出的溶液由异种杀菌剂使用

logging.level.org.apache.http=DEBUG

是好的,但问题是默认情况下Apache HttpComponents是不使用的。

要使用Apache,将HttpComponents添加到pom.xml中

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

并配置RestTemplate:

RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory());

现在最好的解决方案,只需添加依赖项:

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

它包含一个LoggingRequestInterceptor类,你可以这样添加到你的RestTemplate:

通过将它作为拦截器添加到spring RestTemplate中来集成这个实用程序,方法如下:

restTemplate.setRequestFactory(LoggingRequestFactoryFactory.build());

并将slf4j实现添加到您的框架,如log4j。

直接使用“Zg2proRestTemplate”。@PaulSabou的“最佳答案”看起来一般,因为httpclient和所有apache。使用spring RestTemplate时,不一定会加载http库。

奇怪的是,这些解决方案都不能工作,因为RestTemplate似乎不会在某些客户机和服务器上返回500x错误的响应。在这种情况下,您还必须通过实现ResponseErrorHandler来记录这些日志,如下所示。这是一个代码草案,但你明白了:

你可以设置与错误处理程序相同的拦截器:

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

拦截实现了两个接口:

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);
}
}

你可以使用spring-rest-template-logger来记录RestTemplate HTTP流量。

在Maven项目中添加一个依赖项:

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

然后自定义RestTemplate,如下所示:

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

确保在application.properties中启用了调试日志记录:

logging.level.org.hobsoft.spring.resttemplatelogger.LoggingCustomizer = DEBUG

现在所有的RestTemplate HTTP流量将在调试级别被记录到org.hobsoft.spring.resttemplatelogger.LoggingCustomizer

免责声明:这个库是我写的。

日志创建RestTemplate

选项1。打开调试日志记录。

配置创建RestTemplate

  • 默认情况下,创建RestTemplate依赖于标准JDK工具来建立HTTP连接。您可以切换到使用不同的HTTP库,如Apache HttpComponents

    < p > @ bean public RestTemplate RestTemplate (RestTemplateBuilder) RestTemplate = builder.build(); 返回创建restTemplate; 李}< / p > < / >

配置日志记录

  • < p > application.yml

    < p >日志: 水平: 李org.springframework.web.client.RestTemplate:调试< / p > < / >

第二个选项。使用拦截器

包装器响应

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();
}
}

实现拦截器

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;
}


}

配置创建RestTemplate

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

配置日志记录

  • 检查LoggingRestTemplate包,例如在application.yml中:

    < p >日志: 水平: 李com.example.logging:调试< / p > < / >

选项3。使用httpcomponent

导入httpcomponent依赖项

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

配置创建RestTemplate

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

配置日志记录

  • 检查LoggingRestTemplate包,例如在application.yml中:

    < p >日志: 水平: 李org.apache.http:调试< / p > < / >

与使用ClientHttpInterceptor的响应相关,我找到了一种不使用缓冲工厂而保持整个响应的方法。使用一些utils方法将响应体输入流存储在字节数组中,该方法将从正文中复制该数组,但重要的是,用try catch包围该方法,因为如果响应为空,它将中断(这是资源访问异常的原因),并且在catch中只创建空字节数组,然后使用该数组和原始响应中的其他参数创建ClientHttpResponse的匿名内部类。然后,您可以将新的ClientHttpResponse对象返回到剩下的模板执行链,并且可以使用之前存储的主体字节数组记录响应。这样就可以避免在实际响应中使用InputStream,并且可以原原本本地使用Rest Template响应。注意,如果你的响应太大,这可能是危险的

我还想加上这个的实现。我为所有缺少的分号感到抱歉,这是用Groovy编写的。

我需要一些比现有答案更可配置的东西。下面是一个非常敏捷的rest模板bean,它将像OP所寻找的那样记录所有内容。

自定义日志拦截器类:

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模板Bean定义:

@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
}

实现:

@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
}
}

参考Q/A,通过在HttpInputStream上启用多次读取来记录其余模板的请求和响应

为什么我的自定义ClientHttpRequestInterceptor与空响应

这可能不是正确的方法,但我认为这是打印请求和响应而不需要在日志中填充太多内容的最简单方法。

通过添加以下2行应用。Properties记录所有请求和响应,第一行记录请求,第二行记录响应。

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

正如@MilacH指出的,在实现中有一个错误。如果返回statusCode > 400,则从拦截器抛出IOException,因为没有调用errorHandler。可以忽略异常,然后在处理程序方法中再次捕获异常。

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=================================================");
}


}

正如在其他响应中所述,需要对响应体进行特殊处理,以便能够重复读取它(默认情况下,在第一次读取时使用其内容)。

与其在设置请求时使用BufferingClientHttpRequestFactory,拦截器本身可以对响应进行包装,并确保内容被保留,并且可以重复读取(通过记录器和响应的消费者):

我的拦截器

  • 缓冲响应体使用包装器
  • 更紧凑的方式中记录日志
  • 记录状态码标识符以及(例如201创建)
  • 包含请求序列号,允许轻松区分并发日志条目和多个线程

代码:

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();
}
}
}

配置:

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

日志输出示例:

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", ...  }

这里有这么多响应需要修改代码和定制类,但这确实是不必要的。Gte一个调试代理,如fiddler,并设置您的java环境在命令行上使用代理(-Dhttp. exe)。proxyHost和-Dhttp.proxyPort)然后运行fiddler,您可以完整地看到请求和响应。它还带来了许多辅助优势,比如在修改服务器之前,可以在结果和响应发送到运行实验之前和之后对它们进行修补。

可能出现的最后一个问题是,如果必须使用HTTPS,则需要从fiddler导出SSL证书并将其导入java密钥存储库(cacerts)提示:默认的java密钥存储库密码通常是“changeit”。

application.properties

logging.level.org.springframework.web.client=DEBUG

application.yml

logging:
level:
root: WARN
org.springframework.web.client: DEBUG

---- 2019年7月----

(使用Spring Boot)

让我感到惊讶的是,Spring Boot拥有所有的零配置魔法,却没有提供一种使用RestTemplate检查或记录简单JSON响应体的简单方法。我浏览了这里提供的各种答案和评论,并分享了我自己的(仍然)有效的版本,并且在我看来是一个合理的解决方案,考虑到当前的选项(我使用Spring Boot 2.1.6和Gradle 4.4)

1. 使用Fiddler作为http代理

这实际上是一个相当优雅的解决方案,因为它绕过了创建自己的拦截器或将底层http客户端更改为apache的所有繁琐工作(见下文)。

安装并运行提琴手

然后

-DproxySet=true -Dhttp.proxyHost=localhost -Dhttp.proxyPort=8888添加到虚拟机选项中

2. 使用Apache HttpClient

将Apache HttpClient添加到Maven或Gradle依赖项中。

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.9</version>
</dependency>

使用HttpComponentsClientHttpRequestFactory作为RestTemplate的RequestFactory。最简单的方法是:

RestTemplate restTemplate = new RestTemplate();

restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

在你的application.properties文件中启用DEBUG(如果你使用Spring Boot)

logging.level.org.apache.http=DEBUG

如果你正在使用Spring Boot,你需要确保你已经设置了一个日志框架,例如通过使用包含spring-boot-starter-logging的Spring - Boot -starter依赖项。

3.使用拦截器

我会让你通读提案、反提案以及其他答案和评论中的陷阱,然后你自己决定是否要走这条路。

4. 无正文记录URL和响应状态

尽管这并不满足记录主体的要求,但这是开始记录REST调用的一种快速而简单的方法。它显示完整的URL和响应状态。

只需将以下行添加到你的application.properties文件中(假设你正在使用Spring Boot,并假设你正在使用包含spring-boot-starter-logging的Spring引导启动器依赖项)

logging.level.org.springframework.web.client.RestTemplate =调试

输出看起来像这样:

2019-07-29 11:53:50.265 DEBUG o.s.web.client.RestTemplate : HTTP GET http://www.myrestservice.com/Endpoint?myQueryParam=myValue
2019-07-29 11:53:50.276 DEBUG o.s.web.client.RestTemplate : Accept=[application/json]
2019-07-29 11:53:50.584 DEBUG o.s.web.client.RestTemplate : Response 200 OK
2019-07-29 11:53:50.585 DEBUG o.s.web.client.RestTemplate : Reading to [org.mynamespace.MyJsonModelClass]

在Apache HttpClient的帮助下登录Logback:

在类路径中需要Apache HttpClient:

<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.10</version>
</dependency>

配置你的RestTemplate来使用HttpClient:

restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());

要记录请求和响应,请添加到Logback配置文件:

<logger name="org.apache.http.wire" level="DEBUG"/>

或者记录更多:

<logger name="org.apache.http" level="DEBUG"/>

org.apache.http.wire给出了太不可读的日志,所以我使用日志来记录应用程序Servlet和RestTemplate请求。带有有效载荷的响应。

build.gradle:

compile group: 'org.zalando', name: 'logbook-spring-boot-starter', version: '2.6.2'

或Maven依赖:

<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring-boot-starter</artifactId>
<version>2.6.2</version>
</dependency>

application.properties(或槽YAML):

logging.level.org.zalando.logbook = TRACE

RestTemplate.java:

import java.util.function.Supplier;


import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.zalando.logbook.httpclient.LogbookHttpRequestInterceptor;
import org.zalando.logbook.httpclient.LogbookHttpResponseInterceptor;


@Configuration
public class RestTemplateConfiguration {
private final LogbookHttpRequestInterceptor logbookHttpRequestInterceptor;
private final LogbookHttpResponseInterceptor logbookHttpResponseInterceptor;


public RestTemplateConfiguration(LogbookHttpRequestInterceptor logbookHttpRequestInterceptor,
LogbookHttpResponseInterceptor logbookHttpResponseInterceptor) {
this.logbookHttpRequestInterceptor = logbookHttpRequestInterceptor;
this.logbookHttpResponseInterceptor = logbookHttpResponseInterceptor;
}


@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.requestFactory(new MyRequestFactorySupplier())
.build();
}


class MyRequestFactorySupplier implements Supplier<ClientHttpRequestFactory> {
@Override
public ClientHttpRequestFactory get() {
// Using Apache HTTP client
CloseableHttpClient client = HttpClientBuilder.create()
.addInterceptorFirst(logbookHttpRequestInterceptor)
.addInterceptorFirst(logbookHttpResponseInterceptor)
.build();
return new HttpComponentsClientHttpRequestFactory(client);
}
}
}

解决这个问题的简单方法是: < br >

  1. 它将给你更多的控制连接时间和阅读时间。
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(60000))
.setReadTimeout(Duration.ofMillis(60000))
.build();
}


}
    <李> 将这一行添加到resources/application.properties文件中: logging.level.org.springframework.web.client.RestTemplate=DEBUG
    希望问题能得到解决!

我浏览了所有的答案,如果你需要设置认证类型或连接超时,那么你可以这样做:

SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(Integer.valueOf(YOUR_VALUE));
factory.setReadTimeout(Integer.valueOf(YOUR_VALUE));


RestTemplate restTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(factory));
restTemplate.getInterceptors().add(new LoggingRequestInterceptor());
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(USER_NAME,PASSWORD,StandardCharsets.UTF_8));
return restTemplate;