我已经使用Spring RestTemplate有一段时间了,当我试图调试它的请求和响应时,我总是碰壁。我基本上希望看到与打开“verbose”选项时使用curl时相同的东西。例如:
curl -v http://twitter.com/statuses/public_timeline.rss
将显示发送的数据和接收的数据(包括头、cookie等)。
我看了一些相关的帖子,比如:
如何在Spring RestTemplate中记录响应?
但我还没能解决这个问题。
实现这一点的一种方法是实际更改RestTemplate源代码,并在那里添加一些额外的日志记录语句,但我认为这种方法确实是最后的办法。应该有某种方法告诉Spring Web Client/RestTemplate以一种更友好的方式记录所有内容。
我的目标是能够用如下代码做到这一点:
restTemplate.put("http://someurl", objectToPut, urlPathValues);
然后在日志文件或控制台中获得相同类型的调试信息(就像我使用curl获得的一样)。
我相信这对于任何使用Spring RestTemplate并且遇到问题的人来说都是非常有用的。使用curl来调试RestTemplate问题是行不通的(在某些情况下)。
---- 2019年7月----
(使用Spring Boot)
让我感到惊讶的是,Spring Boot拥有所有的零配置魔法,却没有提供一种使用RestTemplate检查或记录简单JSON响应体的简单方法。我浏览了这里提供的各种答案和评论,并分享了我自己的(仍然)有效的版本,并且在我看来是一个合理的解决方案,考虑到当前的选项(我使用Spring Boot 2.1.6和Gradle 4.4)
1. 使用Fiddler作为http代理
这实际上是一个相当优雅的解决方案,因为它绕过了创建自己的拦截器或将底层http客户端更改为apache的所有繁琐工作(见下文)。
安装并运行Fiddler
然后
add -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 = new RestTemplate();
创建restTemplate。setRequestFactory(新HttpComponentsClientHttpRequestFactory ());
在应用程序中启用DEBUG。属性文件(如果您使用Spring Boot)
logging.level.org.apache.http =调试
如果你正在使用Spring Boot,你需要确保你有一个日志框架的设置,例如通过使用一个Spring - Boot -starter依赖项,其中包括Spring - Boot -starter-logging。
3.使用拦截器
我会让你通读提案、反提案以及其他答案和评论中的陷阱,然后你自己决定是否要走这条路。
4. 无正文记录URL和响应状态
尽管这并不满足记录主体的要求,但这是开始记录REST调用的一种快速而简单的方法。它显示完整的URL和响应状态。
只需将以下行添加到应用程序中。properties文件(假设您正在使用Spring Boot,并且假设您正在使用包含Spring - Boot -starter-logging的Spring Boot启动器依赖项)
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]
我的日志记录器配置使用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类型)发送的消息
如果您正在使用任何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();
}
}
我同意花这么多功夫来做这件事很愚蠢。
奇怪的是,这些解决方案都不能工作,因为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);
}
}
现在最好的解决方案,只需添加依赖项:
<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库。
我终于找到了正确的方法。
大部分的解决方案来自
我如何配置Spring和SLF4J以获得日志记录?
看来有两件事需要做:
在log4j中添加以下行。属性:log4j.logger.httpclient.wire=DEBUG
确保spring不会忽略您的日志配置
第二个问题主要发生在使用slf4j的spring环境中(就像我的例子一样)。
因此,当使用slf4j时,请确保发生以下两件事:
There is no commons-logging library in your classpath : this can be done by adding the exclusion descriptors in your pom :
<exclusions><exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
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)