Logging improvements for WebFlux

Issue: SPR-16898
This commit is contained in:
Rossen Stoyanchev
2018-06-22 22:41:30 -04:00
parent eaffcbe3be
commit 900bc8a2e3
109 changed files with 1106 additions and 671 deletions

View File

@@ -86,7 +86,7 @@ public abstract class HttpAccessor {
protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
ClientHttpRequest request = getRequestFactory().createRequest(url, method);
if (logger.isDebugEnabled()) {
logger.debug("Created " + method.name() + " request for \"" + url + "\"");
logger.debug("HTTP " + method.name() + " " + url);
}
return request;
}

View File

@@ -109,6 +109,21 @@ public interface CodecConfigurer {
* @see org.springframework.http.codec.json.Jackson2JsonEncoder
*/
void jackson2JsonEncoder(Encoder<?> encoder);
/**
* Whether to disable logging of request details for form and multipart
* requests at any log level. By default such data is logged under
* {@code "org.springframework.http.codec"} but may contain sensitive
* information. Typically that's not an issue since DEBUG is used in
* development, but this option may be used to explicitly disable any
* logging of form and multipart data at any log level.
* <p>By default this is set to {@code false} in which case form and
* multipart data is logged at DEBUG or TRACE. When set to {@code true}
* values will not be logged at any level.
* @param disableLoggingRequestDetails whether to disable loggins
* @since 5.1
*/
void disableLoggingRequestDetails(boolean disableLoggingRequestDetails);
}

View File

@@ -25,6 +25,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -46,13 +48,15 @@ import org.springframework.util.StringUtils;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class FormHttpMessageReader implements HttpMessageReader<MultiValueMap<String, String>> {
public class FormHttpMessageReader extends LoggingCodecSupport
implements HttpMessageReader<MultiValueMap<String, String>> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final ResolvableType MULTIVALUE_TYPE =
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
private Charset defaultCharset = DEFAULT_CHARSET;
@@ -101,7 +105,11 @@ public class FormHttpMessageReader implements HttpMessageReader<MultiValueMap<St
CharBuffer charBuffer = charset.decode(buffer.asByteBuffer());
String body = charBuffer.toString();
DataBufferUtils.release(buffer);
return parseFormData(charset, body);
MultiValueMap<String, String> formData = parseFormData(charset, body);
if (shouldLogRequestDetails()) {
logger.debug("Decoded " + formData);
}
return formData;
});
}

View File

@@ -22,10 +22,11 @@ import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@@ -57,7 +58,8 @@ import org.springframework.util.MultiValueMap;
* @since 5.0
* @see org.springframework.http.codec.multipart.MultipartHttpMessageWriter
*/
public class FormHttpMessageWriter implements HttpMessageWriter<MultiValueMap<String, String>> {
public class FormHttpMessageWriter extends LoggingCodecSupport
implements HttpMessageWriter<MultiValueMap<String, String>> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
@@ -127,12 +129,15 @@ public class FormHttpMessageWriter implements HttpMessageWriter<MultiValueMap<St
Assert.notNull(charset, "No charset"); // should never occur
return Mono.from(inputStream).flatMap(form -> {
String value = serializeForm(form, charset);
ByteBuffer byteBuffer = charset.encode(value);
DataBuffer buffer = message.bufferFactory().wrap(byteBuffer);
message.getHeaders().setContentLength(byteBuffer.remaining());
return message.writeWith(Mono.just(buffer));
});
if (shouldLogRequestDetails()) {
logger.debug("Encoding " + form);
}
String value = serializeForm(form, charset);
ByteBuffer byteBuffer = charset.encode(value);
DataBuffer buffer = message.bufferFactory().wrap(byteBuffer);
message.getHeaders().setContentLength(byteBuffer.remaining());
return message.writeWith(Mono.just(buffer));
});
}
private MediaType getMediaType(@Nullable MediaType mediaType) {

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.codec;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Base class for {@link org.springframework.core.codec.Encoder},
* {@link org.springframework.core.codec.Decoder}, {@link HttpMessageReader}, or
* {@link HttpMessageWriter} that uses a logger and shows potentially sensitive
* request data.
*
* @author Rossen Stoyanchev
* @since 5.1
*/
public class LoggingCodecSupport {
protected final Log logger = LogFactory.getLog(getClass());
/** Do not log potentially sensitive information (params at DEBUG and headers at TRACE). */
private boolean disableLoggingRequestDetails = false;
/**
* Whether to disable any logging of request details by this codec.
* By default values being encoded or decoded are logged at DEBUG and TRACE
* level under {@code "org.springframework.http.codec"} which may show
* sensitive data for form and multipart data. Typically that's not an issue
* since DEBUG and TRACE are intended for development, but this property may
* be used to explicitly disable any logging of such information regardless
* of the log level.
* <p>By default this is set to {@code false} in which case values encoded
* or decoded are logged at DEBUG level. When set to {@code true} values
* will not be logged at any level.
* @param disableLoggingRequestDetails whether to disable
*/
public void setDisableLoggingRequestDetails(boolean disableLoggingRequestDetails) {
this.disableLoggingRequestDetails = disableLoggingRequestDetails;
}
/**
* Whether any logging of values being encoded or decoded is explicitly
* disabled regardless of log level.
*/
public boolean isDisableLoggingRequestDetails() {
return this.disableLoggingRequestDetails;
}
/**
* Returns "true" if logger is at DEBUG level and the logging of values
* being encoded or decoded is not explicitly disabled.
*/
protected boolean shouldLogRequestDetails() {
return !this.disableLoggingRequestDetails && logger.isDebugEnabled();
}
}

View File

@@ -23,6 +23,8 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -72,6 +74,8 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
private static final ResolvableType REGION_TYPE = ResolvableType.forClass(ResourceRegion.class);
private static final Log logger = LogFactory.getLog(ResourceHttpMessageWriter.class);
private final ResourceEncoder encoder;
@@ -139,7 +143,11 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
if (mediaType != null && mediaType.isConcrete() && !mediaType.equals(MediaType.APPLICATION_OCTET_STREAM)) {
return mediaType;
}
return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
mediaType = MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
if (logger.isDebugEnabled()) {
logger.debug("Resource associated with '" + mediaType + "'");
}
return mediaType;
}
private static long lengthOf(Resource resource) {
@@ -162,6 +170,10 @@ public class ResourceHttpMessageWriter implements HttpMessageWriter<Resource> {
File file = resource.getFile();
long pos = region != null ? region.getPosition() : 0;
long count = region != null ? region.getCount() : file.length();
if (logger.isDebugEnabled()) {
String formatted = region != null ? "region " + pos + "-" + (count) + " of " : "";
logger.debug("Zero-copy " + formatted + "[" + resource + "]");
}
return Optional.of(((ZeroCopyHttpOutputMessage) message).writeWith(file, pos, count));
}
catch (IOException ex) {

View File

@@ -110,7 +110,11 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple
return tokens.map(tokenBuffer -> {
try {
return reader.readValue(tokenBuffer.asParser(getObjectMapper()));
Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()));
if (logger.isDebugEnabled()) {
logger.debug("Decoded [" + value + "]");
}
return value;
}
catch (InvalidDefinitionException ex) {
throw new CodecException("Type definition error: " + ex.getType(), ex);

View File

@@ -33,6 +33,7 @@ import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import org.apache.commons.logging.Log;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -140,6 +141,11 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
private DataBuffer encodeValue(Object value, @Nullable MimeType mimeType, DataBufferFactory bufferFactory,
ResolvableType elementType, @Nullable Map<String, Object> hints, JsonEncoding encoding) {
Log logger = getLogger(hints);
if (logger.isDebugEnabled()) {
logger.debug("Encoding [" + value + "]");
}
JavaType javaType = getJavaType(elementType.getType(), null);
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);
ObjectWriter writer = (jsonView != null ?

View File

@@ -28,6 +28,8 @@ import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
@@ -62,6 +64,8 @@ public abstract class Jackson2CodecSupport {
new MimeType("application", "*+json", StandardCharsets.UTF_8)));
protected final Log logger = LogFactory.getLog(getClass());
private final ObjectMapper objectMapper;
private final List<MimeType> mimeTypes;
@@ -94,6 +98,19 @@ public abstract class Jackson2CodecSupport {
return (mimeType == null || this.mimeTypes.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
}
/**
* Helper method to obtain the logger to use from the Map of hints, or fall
* back on the default logger. This may be used for example to override
* logging, e.g. for a multipart request where the full map of part values
* has already been logged.
* @param hints the hints passed to the encode method
* @return the logger to use
* @since 5.1
*/
protected Log getLogger(@Nullable Map<String, Object> hints) {
return hints != null ? ((Log) hints.getOrDefault(Log.class.getName(), logger)) : logger;
}
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));

View File

@@ -29,6 +29,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.impl.NoOpLog;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -48,6 +50,7 @@ import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.FormHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.LoggingCodecSupport;
import org.springframework.http.codec.ResourceHttpMessageWriter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -75,10 +78,15 @@ import org.springframework.util.MultiValueMap;
* @since 5.0
* @see FormHttpMessageWriter
*/
public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueMap<String, ?>> {
public class MultipartHttpMessageWriter extends LoggingCodecSupport
implements HttpMessageWriter<MultiValueMap<String, ?>> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
/** Suppress logging from individual part writers (full map logged at this level) */
private static final Map<String, Object> DEFAULT_HINTS =
Collections.singletonMap(Log.class.getName(), new NoOpLog());
private final List<HttpMessageWriter<?>> partWriters;
@@ -214,6 +222,10 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
outputMessage.getHeaders().setContentType(new MediaType(MediaType.MULTIPART_FORM_DATA, params));
if (shouldLogRequestDetails()) {
logger.debug("Encoding " + map);
}
Flux<DataBuffer> body = Flux.fromIterable(map.entrySet())
.concatMap(entry -> encodePartValues(boundary, entry.getKey(), entry.getValue()))
.concatWith(Mono.just(generateLastLine(boundary)));
@@ -291,7 +303,7 @@ public class MultipartHttpMessageWriter implements HttpMessageWriter<MultiValueM
// but only stores the body Flux and returns Mono.empty().
Mono<Void> partContentReady = ((HttpMessageWriter<T>) writer.get())
.write(bodyPublisher, resolvableType, contentType, outputMessage, Collections.emptyMap());
.write(bodyPublisher, resolvableType, contentType, outputMessage, DEFAULT_HINTS);
// After partContentReady, we can access the part content from MultipartHttpOutputMessage
// and use it for writing to the actual request body

View File

@@ -32,6 +32,8 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.synchronoss.cloud.nio.multipart.DefaultPartBodyStreamStorageFactory;
import org.synchronoss.cloud.nio.multipart.Multipart;
import org.synchronoss.cloud.nio.multipart.MultipartContext;
@@ -53,6 +55,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.LoggingCodecSupport;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -70,7 +73,7 @@ import org.springframework.util.Assert;
* @see <a href="https://github.com/synchronoss/nio-multipart">Synchronoss NIO Multipart</a>
* @see MultipartHttpMessageReader
*/
public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part> {
public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implements HttpMessageReader<Part> {
private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@@ -91,7 +94,12 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
@Override
public Flux<Part> read(ResolvableType elementType, ReactiveHttpInputMessage message, Map<String, Object> hints) {
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory));
return Flux.create(new SynchronossPartGenerator(message, this.bufferFactory, this.streamStorageFactory))
.doOnNext(part -> {
if (shouldLogRequestDetails()) {
logger.debug("Decoded [" + part + "]");
}
});
}
@@ -263,6 +271,11 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
DataBufferFactory getBufferFactory() {
return this.bufferFactory;
}
@Override
public String toString() {
return "Part '" + this.name + "', headers=" + this.headers;
}
}
@@ -342,6 +355,11 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
}
return Mono.empty();
}
@Override
public String toString() {
return "Part '" + name() + "', filename='" + this.filename + "'";
}
}
@@ -371,6 +389,11 @@ public class SynchronossPartHttpMessageReader implements HttpMessageReader<Part>
String name = MultipartUtils.getCharEncoding(headers());
return (name != null ? Charset.forName(name) : StandardCharsets.UTF_8);
}
@Override
public String toString() {
return "Part '" + name() + "=" + this.content + "'";
}
}
}

View File

@@ -74,6 +74,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
@Nullable
private Encoder<?> jackson2JsonEncoder;
private boolean disableLoggingRequestDetails = false;
private boolean registerDefaults = true;
@@ -87,6 +89,15 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
this.jackson2JsonEncoder = encoder;
}
@Override
public void disableLoggingRequestDetails(boolean disableLoggingRequestDetails) {
this.disableLoggingRequestDetails = disableLoggingRequestDetails;
}
protected boolean isDisableLoggingRequestDetails() {
return this.disableLoggingRequestDetails;
}
/**
* Delegate method used from {@link BaseCodecConfigurer#registerDefaults}.
*/
@@ -108,8 +119,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs {
readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder()));
readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder()));
readers.add(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly()));
readers.add(new FormHttpMessageReader());
FormHttpMessageReader formReader = new FormHttpMessageReader();
formReader.setDisableLoggingRequestDetails(this.disableLoggingRequestDetails);
readers.add(formReader);
extendTypedReaders(readers);
return readers;
}

View File

@@ -86,7 +86,14 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
@Override
protected void extendTypedWriters(List<HttpMessageWriter<?>> typedWriters) {
typedWriters.add(new MultipartHttpMessageWriter(getPartWriters(), new FormHttpMessageWriter()));
FormHttpMessageWriter formWriter = new FormHttpMessageWriter();
formWriter.setDisableLoggingRequestDetails(isDisableLoggingRequestDetails());
MultipartHttpMessageWriter multipartWriter = new MultipartHttpMessageWriter(getPartWriters(), formWriter);
multipartWriter.setDisableLoggingRequestDetails(isDisableLoggingRequestDetails());
typedWriters.add(multipartWriter);
}
private List<HttpMessageWriter<?>> getPartWriters() {

View File

@@ -51,6 +51,7 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
protected void extendTypedReaders(List<HttpMessageReader<?>> typedReaders) {
if (synchronossMultipartPresent) {
SynchronossPartHttpMessageReader partReader = new SynchronossPartHttpMessageReader();
partReader.setDisableLoggingRequestDetails(isDisableLoggingRequestDetails());
typedReaders.add(partReader);
typedReaders.add(new MultipartHttpMessageReader(partReader));
}

View File

@@ -104,7 +104,13 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
QName typeName = toQName(outputClass);
Flux<List<XMLEvent>> splitEvents = split(xmlEventFlux, typeName);
return splitEvents.map(events -> unmarshal(events, outputClass));
return splitEvents.map(events -> {
Object value = unmarshal(events, outputClass);
if (logger.isDebugEnabled()) {
logger.debug("Decoded [" + value + "]");
}
return value;
});
}
@Override

View File

@@ -25,6 +25,7 @@ import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.apache.commons.logging.Log;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
@@ -73,6 +74,10 @@ public class Jaxb2XmlEncoder extends AbstractSingleValueEncoder<Object> {
protected Flux<DataBuffer> encode(Object value, DataBufferFactory dataBufferFactory,
ResolvableType type, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
try {
Log logger = getLogger(hints);
if (logger.isDebugEnabled()) {
logger.debug("Encoding [" + value + "]");
}
DataBuffer buffer = dataBufferFactory.allocateBuffer(1024);
OutputStream outputStream = buffer.asOutputStream();
Class<?> clazz = ClassUtils.getUserClass(value);

View File

@@ -85,7 +85,7 @@ class DefaultPathContainer implements PathContainer {
@Override
public String toString() {
return "[path='" + this.path + "\']";
return value();
}

View File

@@ -148,9 +148,7 @@ class DefaultRequestPath implements RequestPath {
@Override
public String toString() {
return "DefaultRequestPath[fullPath='" + this.fullPath + "', " +
"contextPath='" + this.contextPath.value() + "', " +
"pathWithinApplication='" + this.pathWithinApplication.value() + "']";
return this.fullPath.toString();
}
}

View File

@@ -88,16 +88,15 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
}
@Override
public boolean setStatusCode(@Nullable HttpStatus statusCode) {
public boolean setStatusCode(@Nullable HttpStatus status) {
if (this.state.get() == State.COMMITTED) {
if (logger.isTraceEnabled()) {
logger.trace("HTTP response already committed. " +
"Status not set to " + (statusCode != null ? statusCode.toString() : "null"));
logger.trace("Ignoring status " + status + ": response already committed");
}
return false;
}
else {
this.statusCode = (statusCode != null ? statusCode.value() : null);
this.statusCode = (status != null ? status.value() : null);
return true;
}
}
@@ -203,9 +202,6 @@ public abstract class AbstractServerHttpResponse implements ServerHttpResponse {
*/
protected Mono<Void> doCommit(@Nullable Supplier<? extends Mono<Void>> writeAction) {
if (!this.state.compareAndSet(State.NEW, State.COMMITTING)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping doCommit (response already committed).");
}
return Mono.empty();
}

View File

@@ -61,8 +61,8 @@ public class ReactorHttpHandlerAdapter implements BiFunction<HttpServerRequest,
adaptedResponse = new ReactorServerHttpResponse(response, bufferFactory);
}
catch (URISyntaxException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Invalid URL for incoming request: " + ex.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("Failed to get request URI: " + ex.getMessage());
}
response.status(HttpResponseStatus.BAD_REQUEST);
return Mono.empty();
@@ -73,8 +73,8 @@ public class ReactorHttpHandlerAdapter implements BiFunction<HttpServerRequest,
}
return this.httpHandler.handle(adaptedRequest, adaptedResponse)
.doOnError(ex -> logger.warn("Handling completed with error: " + ex.getMessage()))
.doOnSuccess(aVoid -> logger.debug("Handling completed with success"));
.doOnError(ex -> logger.trace("Failed to complete: " + ex.getMessage()))
.doOnSuccess(aVoid -> logger.trace("Handling completed"));
}
}

View File

@@ -141,7 +141,7 @@ public class ServletHttpHandlerAdapter implements Servlet {
if (mapping.endsWith("/*")) {
String path = mapping.substring(0, mapping.length() - 2);
if (!path.isEmpty()) {
logger.info("Found Servlet mapping '" + path + "' for Servlet '" + name + "'");
logger.info("Found servlet mapping prefix '" + path + "' for '" + name + "'");
}
return path;
}
@@ -159,7 +159,7 @@ public class ServletHttpHandlerAdapter implements Servlet {
if (DispatcherType.ASYNC.equals(request.getDispatcherType())) {
Throwable ex = (Throwable) request.getAttribute(WRITE_ERROR_ATTRIBUTE_NAME);
throw new ServletException("Write publisher error", ex);
throw new ServletException("Failed to create response content", ex);
}
// Start async before Read/WriteListener registration
@@ -171,8 +171,8 @@ public class ServletHttpHandlerAdapter implements Servlet {
httpRequest = createRequest(((HttpServletRequest) request), asyncContext);
}
catch (URISyntaxException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Invalid URL for incoming request: " + ex.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("Failed to get request URL: " + ex.getMessage());
}
((HttpServletResponse) response).setStatus(400);
asyncContext.complete();
@@ -247,14 +247,15 @@ public class ServletHttpHandlerAdapter implements Servlet {
@Override
public void onTimeout(AsyncEvent event) {
logger.debug("Timeout notification from Servlet container");
logger.debug("Timeout notification");
AsyncContext context = event.getAsyncContext();
runIfAsyncNotComplete(context, this.isCompleted, context::complete);
}
@Override
public void onError(AsyncEvent event) {
logger.debug("Error notification from Servlet container");
Throwable ex = event.getThrowable();
logger.debug("Error notification: " + (ex != null ? ex : "<no Throwable>"));
AsyncContext context = event.getAsyncContext();
runIfAsyncNotComplete(context, this.isCompleted, context::complete);
}
@@ -294,16 +295,16 @@ public class ServletHttpHandlerAdapter implements Servlet {
@Override
public void onError(Throwable ex) {
logger.warn("Handling completed with error: " + ex.getMessage());
logger.trace("Failed to complete: " + ex.getMessage());
runIfAsyncNotComplete(this.asyncContext, this.isCompleted, () -> {
if (this.asyncContext.getResponse().isCommitted()) {
logger.debug("Dispatching into container to raise error");
logger.trace("Dispatch to container, to raise the error on servlet thread");
this.asyncContext.getRequest().setAttribute(WRITE_ERROR_ATTRIBUTE_NAME, ex);
this.asyncContext.dispatch();
}
else {
try {
logger.debug("Setting response status code to 500");
logger.trace("Setting ServletResponse status to 500 Server Error");
this.asyncContext.getResponse().resetBuffer();
((HttpServletResponse) this.asyncContext.getResponse()).setStatus(500);
}
@@ -316,7 +317,7 @@ public class ServletHttpHandlerAdapter implements Servlet {
@Override
public void onComplete() {
logger.debug("Handling completed with success");
logger.trace("Handling completed");
runIfAsyncNotComplete(this.asyncContext, this.isCompleted, this.asyncContext::complete);
}
}

View File

@@ -204,7 +204,7 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest {
DataBuffer readFromInputStream() throws IOException {
int read = this.request.getInputStream().read(this.buffer);
if (logger.isTraceEnabled()) {
logger.trace("InputStream read returned " + read + (read != -1 ? " bytes" : ""));
logger.trace("InputStream.read returned " + read + (read != -1 ? " bytes" : ""));
}
if (read > 0) {

View File

@@ -72,7 +72,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
}
catch (URISyntaxException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Invalid URL for incoming request: " + ex.getMessage());
logger.debug("Failed to get request URI: " + ex.getMessage());
}
exchange.setStatusCode(400);
return;
@@ -108,7 +108,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
@Override
public void onError(Throwable ex) {
logger.warn("Handling completed with error: " + ex.getMessage());
logger.trace("Failed to complete: " + ex.getMessage());
if (this.exchange.isResponseStarted()) {
try {
logger.debug("Closing connection");
@@ -119,7 +119,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
}
}
else {
logger.debug("Setting response status code to 500");
logger.debug("Setting HttpServerExchange status to 500 Server Error");
this.exchange.setStatusCode(500);
this.exchange.endExchange();
}
@@ -127,7 +127,7 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
@Override
public void onComplete() {
logger.debug("Handling completed with success");
logger.trace("Handling completed");
this.exchange.endExchange();
}
}

View File

@@ -174,7 +174,7 @@ class UndertowServerHttpRequest extends AbstractServerHttpRequest {
int read = this.channel.read(byteBuffer);
if (logger.isTraceEnabled()) {
logger.trace("Channel read returned " + read + (read != -1 ? " bytes" : ""));
logger.trace("Channel.read returned " + read + (read != -1 ? " bytes" : ""));
}
if (read > 0) {

View File

@@ -94,8 +94,7 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
(GenericHttpMessageConverter<?>) messageConverter;
if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + this.responseType + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
logger.debug("Reading [" + this.responseType + "]");
}
return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
}
@@ -103,8 +102,8 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
if (this.responseClass != null) {
if (messageConverter.canRead(this.responseClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + this.responseClass.getName() + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
String className = this.responseClass.getName();
logger.debug("Reading [" + className + "] as \"" + contentType + "\"");
}
return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
}
@@ -131,7 +130,7 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
MediaType contentType = response.getHeaders().getContentType();
if (contentType == null) {
if (logger.isTraceEnabled()) {
logger.trace("No Content-Type header found, defaulting to application/octet-stream");
logger.trace("No content-type, using 'application/octet-stream'");
}
contentType = MediaType.APPLICATION_OCTET_STREAM;
}

View File

@@ -32,6 +32,7 @@ import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
@@ -789,9 +790,9 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
boolean hasError = errorHandler.hasError(response);
if (logger.isDebugEnabled()) {
try {
logger.debug(method.name() + " request for \"" + url + "\" resulted in " +
response.getRawStatusCode() + " (" + response.getStatusText() + ")" +
(hasError ? "; invoking error handler" : ""));
int code = response.getRawStatusCode();
HttpStatus status = HttpStatus.resolve(code);
logger.debug("Response " + (status != null ? status : code));
}
catch (IOException ex) {
// ignore
@@ -873,7 +874,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
.sorted(MediaType.SPECIFICITY_COMPARATOR)
.collect(Collectors.toList());
if (logger.isDebugEnabled()) {
logger.debug("Setting request Accept header to " + allSupportedMediaTypes);
logger.debug("Accept=" + allSupportedMediaTypes);
}
request.getHeaders().setAccept(allSupportedMediaTypes);
}
@@ -958,16 +959,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
if (!requestHeaders.isEmpty()) {
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
}
if (logger.isDebugEnabled()) {
if (requestContentType != null) {
logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
"\" using [" + messageConverter + "]");
}
else {
logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
}
}
logBody(requestBody, requestContentType, genericConverter);
genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
return;
}
@@ -976,29 +968,31 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
if (!requestHeaders.isEmpty()) {
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
}
if (logger.isDebugEnabled()) {
if (requestContentType != null) {
logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
"\" using [" + messageConverter + "]");
}
else {
logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
}
}
logBody(requestBody, requestContentType, messageConverter);
((HttpMessageConverter<Object>) messageConverter).write(
requestBody, requestContentType, httpRequest);
return;
}
}
String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +
requestBodyClass.getName() + "]";
String message = "No HttpMessageConverter for [" + requestBodyClass.getName() + "]";
if (requestContentType != null) {
message += " and content type [" + requestContentType + "]";
}
throw new RestClientException(message);
}
}
private void logBody(Object body, @Nullable MediaType mediaType, HttpMessageConverter<?> converter) {
if (logger.isDebugEnabled()) {
if (mediaType != null) {
logger.debug("Writing [" + body + "] as \"" + mediaType + "\"");
}
else {
String classname = converter.getClass().getName();
logger.debug("Writing [" + body + "] with " + classname);
}
}
}
}

View File

@@ -75,7 +75,9 @@ class CallableInterceptorChain {
catch (Throwable ex) {
// Save the first exception but invoke all interceptors
if (exceptionResult != null) {
logger.warn("Unhandled error from interceptor postProcess method", ex);
if (logger.isTraceEnabled()) {
logger.trace("Ignoring failure in postProcess method", ex);
}
}
else {
exceptionResult = ex;
@@ -141,7 +143,9 @@ class CallableInterceptorChain {
this.interceptors.get(i).afterCompletion(request, task);
}
catch (Throwable ex) {
logger.error("Unhandled error from interceptor afterCompletion method", ex);
if (logger.isTraceEnabled()) {
logger.trace("Ignoring failure in afterCompletion method", ex);
}
}
}
}

View File

@@ -209,7 +209,7 @@ public class DeferredResult<T> {
resultHandler.handleResult(resultToHandle);
}
catch (Throwable ex) {
logger.debug("Failed to handle existing result", ex);
logger.debug("Failed to process async result", ex);
}
}

View File

@@ -106,7 +106,7 @@ class DeferredResultInterceptorChain {
this.interceptors.get(i).afterCompletion(request, deferredResult);
}
catch (Throwable ex) {
logger.error("Unhandled error from interceptor afterCompletion method", ex);
logger.trace("Ignoring failure in afterCompletion method", ex);
}
}
}

View File

@@ -34,7 +34,6 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler;
import org.springframework.web.util.UrlPathHelper;
/**
* The central class for managing asynchronous request processing, mainly intended
@@ -64,8 +63,6 @@ public final class WebAsyncManager {
private static final Log logger = LogFactory.getLog(WebAsyncManager.class);
private static final UrlPathHelper urlPathHelper = new UrlPathHelper();
private static final CallableProcessingInterceptor timeoutCallableInterceptor =
new TimeoutCallableProcessingInterceptor();
@@ -335,7 +332,7 @@ public final class WebAsyncManager {
private String formatRequestUri() {
HttpServletRequest request = this.asyncWebRequest.getNativeRequest(HttpServletRequest.class);
return request != null ? urlPathHelper.getRequestUri(request) : "servlet container";
return request != null ? request.getRequestURI() : "servlet container";
}
private void setConcurrentResultAndDispatch(Object result) {
@@ -347,7 +344,9 @@ public final class WebAsyncManager {
}
if (this.asyncWebRequest.isAsyncComplete()) {
logger.error("Could not complete async processing due to timeout or network error");
if (logger.isDebugEnabled()) {
logger.debug("Async request already complete for " + formatRequestUri());
}
return;
}

View File

@@ -68,13 +68,13 @@ public class DefaultCorsProcessor implements CorsProcessor {
ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
if (responseHasCors(serverResponse)) {
logger.trace("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header");
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
if (WebUtils.isSameOrigin(serverRequest)) {
logger.trace("Skip CORS processing: request is from same origin");
logger.trace("Skip: request is from same origin");
return true;
}
@@ -126,7 +126,7 @@ public class DefaultCorsProcessor implements CorsProcessor {
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
if (allowOrigin == null) {
logger.debug("Rejecting CORS request because '" + requestOrigin + "' origin is not allowed");
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
@@ -134,7 +134,7 @@ public class DefaultCorsProcessor implements CorsProcessor {
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Rejecting CORS request because '" + requestMethod + "' request method is not allowed");
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
@@ -142,7 +142,7 @@ public class DefaultCorsProcessor implements CorsProcessor {
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Rejecting CORS request because '" + requestHeaders + "' request headers are not allowed");
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}

View File

@@ -63,12 +63,12 @@ public class DefaultCorsProcessor implements CorsProcessor {
}
if (responseHasCors(response)) {
logger.debug("Skip CORS: response already contains \"Access-Control-Allow-Origin\" header");
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
if (CorsUtils.isSameOrigin(request)) {
logger.debug("Skip CORS: request is from same origin");
logger.trace("Skip: request is from same origin");
return true;
}
@@ -95,7 +95,6 @@ public class DefaultCorsProcessor implements CorsProcessor {
*/
protected void rejectRequest(ServerHttpResponse response) {
response.setStatusCode(HttpStatus.FORBIDDEN);
logger.debug("Invalid CORS request");
}
/**
@@ -114,7 +113,7 @@ public class DefaultCorsProcessor implements CorsProcessor {
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
if (allowOrigin == null) {
logger.debug("Rejecting CORS request because '" + requestOrigin + "' origin is not allowed");
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
@@ -122,7 +121,7 @@ public class DefaultCorsProcessor implements CorsProcessor {
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Rejecting CORS request because '" + requestMethod + "' request method is not allowed");
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
@@ -130,7 +129,7 @@ public class DefaultCorsProcessor implements CorsProcessor {
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Rejecting CORS request because '" + requestHeaders + "' request headers are not allowed");
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}

View File

@@ -209,9 +209,6 @@ public abstract class GenericFilterBean implements Filter, BeanNameAware, Enviro
@Override
public final void init(FilterConfig filterConfig) throws ServletException {
Assert.notNull(filterConfig, "FilterConfig must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Initializing filter '" + filterConfig.getFilterName() + "'");
}
this.filterConfig = filterConfig;
@@ -241,7 +238,7 @@ public abstract class GenericFilterBean implements Filter, BeanNameAware, Enviro
initFilterBean();
if (logger.isDebugEnabled()) {
logger.debug("Filter '" + filterConfig.getFilterName() + "' configured successfully");
logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use");
}
}

View File

@@ -100,8 +100,8 @@ public class RequestContextFilter extends OncePerRequestFilter {
}
finally {
resetContextHolders();
if (logger.isDebugEnabled()) {
logger.debug("Cleared thread-bound request context: " + request);
if (logger.isTraceEnabled()) {
logger.trace("Cleared thread-bound request context: " + request);
}
attributes.requestCompleted();
}
@@ -110,8 +110,8 @@ public class RequestContextFilter extends OncePerRequestFilter {
private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
if (logger.isDebugEnabled()) {
logger.debug("Bound request context to thread: " + request);
if (logger.isTraceEnabled()) {
logger.trace("Bound request context to thread: " + request);
}
}

View File

@@ -128,23 +128,13 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
if (requestETag != null && ("*".equals(requestETag) || responseETag.equals(requestETag) ||
responseETag.replaceFirst("^W/", "").equals(requestETag.replaceFirst("^W/", "")))) {
if (logger.isTraceEnabled()) {
logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304");
}
rawResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
else {
if (logger.isTraceEnabled()) {
logger.trace("ETag [" + responseETag + "] not equal to If-None-Match [" + requestETag +
"], sending normal response");
}
responseWrapper.copyBodyToResponse();
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Response with status code [" + statusCode + "] not eligible for ETag");
}
responseWrapper.copyBodyToResponse();
}
}

View File

@@ -155,18 +155,11 @@ public final class ModelFactory {
private ModelMethod getNextModelMethod(ModelAndViewContainer container) {
for (ModelMethod modelMethod : this.modelMethods) {
if (modelMethod.checkDependencies(container)) {
if (logger.isTraceEnabled()) {
logger.trace("Selected @ModelAttribute method " + modelMethod);
}
this.modelMethods.remove(modelMethod);
return modelMethod;
}
}
ModelMethod modelMethod = this.modelMethods.get(0);
if (logger.isTraceEnabled()) {
logger.trace("Selected @ModelAttribute method (not present: " +
modelMethod.getUnresolvedDependencies(container)+ ") " + modelMethod);
}
this.modelMethods.remove(modelMethod);
return modelMethod;
}

View File

@@ -132,10 +132,6 @@ public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgu
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (logger.isTraceEnabled()) {
logger.trace("Testing if argument resolver [" + methodArgumentResolver + "] supports [" +
parameter.getGenericParameterType() + "]");
}
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);

View File

@@ -19,6 +19,8 @@ package org.springframework.web.method.support;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
@@ -131,15 +133,9 @@ public class InvocableHandlerMethod extends HandlerMethod {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) +
"' with arguments " + Arrays.toString(args));
logger.trace("Arguments: " + Arrays.toString(args));
}
Object returnValue = doInvoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) +
"] returned [" + returnValue + "]");
}
return returnValue;
return doInvoke(args);
}
/**
@@ -214,7 +210,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
throw new IllegalStateException(getInvocationErrorMessage(text, args), ex);
throw new IllegalStateException(formatInvokeError(text, args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
@@ -229,8 +225,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
throw (Exception) targetException;
}
else {
String text = getInvocationErrorMessage("Failed to invoke handler method", args);
throw new IllegalStateException(text, targetException);
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
}
}
}
@@ -250,36 +245,22 @@ public class InvocableHandlerMethod extends HandlerMethod {
"' is not an instance of the actual controller bean class '" +
targetBeanClass.getName() + "'. If the controller requires proxying " +
"(e.g. due to @Transactional), please use class-based proxying.";
throw new IllegalStateException(getInvocationErrorMessage(text, args));
throw new IllegalStateException(formatInvokeError(text, args));
}
}
private String getInvocationErrorMessage(String text, Object[] resolvedArgs) {
StringBuilder sb = new StringBuilder(getDetailedErrorMessage(text));
sb.append("Resolved arguments: \n");
for (int i = 0; i < resolvedArgs.length; i++) {
sb.append("[").append(i).append("] ");
if (resolvedArgs[i] == null) {
sb.append("[null] \n");
}
else {
sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] ");
sb.append("[value=").append(resolvedArgs[i]).append("]\n");
}
}
return sb.toString();
}
private String formatInvokeError(String text, Object[] args) {
/**
* Adds HandlerMethod details such as the bean type and method signature to the message.
* @param text error message to append the HandlerMethod details to
*/
protected String getDetailedErrorMessage(String text) {
StringBuilder sb = new StringBuilder(text).append("\n");
sb.append("HandlerMethod details: \n");
sb.append("Controller [").append(getBeanType().getName()).append("]\n");
sb.append("Method [").append(getBridgedMethod().toGenericString()).append("]\n");
return sb.toString();
String formattedArgs = IntStream.range(0, args.length)
.mapToObj(i -> (args[i] != null ?
"[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" :
"[" + i + "] [null]"))
.collect(Collectors.joining(",\n", " ", " "));
return text + "\n" +
"Controller [" + getBeanType().getName() + "]\n" +
"Method [" + getBridgedMethod().toGenericString() + "] " +
"with argument values:\n" + formattedArgs;
}
}

View File

@@ -88,8 +88,7 @@ public class ResponseStatusException extends NestedRuntimeException {
@Override
public String getMessage() {
String msg = "Response status " + this.status +
(this.reason != null ? " with reason \"" + reason + "\"" : "");
String msg = this.status + (this.reason != null ? " \"" + reason + "\"" : "");
return NestedExceptionUtils.buildMessage(msg, getCause());
}

View File

@@ -27,12 +27,14 @@ import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.LoggingCodecSupport;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.handler.WebHandlerDecorator;
@@ -80,20 +82,21 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
private static final Log logger = LogFactory.getLog(HttpWebHandlerAdapter.class);
private static final Log disconnectedClientLogger = LogFactory.getLog(DISCONNECTED_CLIENT_LOG_CATEGORY);
private static final Log lostClientLogger = LogFactory.getLog(DISCONNECTED_CLIENT_LOG_CATEGORY);
private WebSessionManager sessionManager = new DefaultWebSessionManager();
@Nullable
private ServerCodecConfigurer codecConfigurer;
private ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
@Nullable
private LocaleContextResolver localeContextResolver;
private LocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
@Nullable
private ApplicationContext applicationContext;
/** Do not log potentially sensitive data (query/form at DEBUG, headers at TRACE). */
private boolean disableLoggingRequestDetails = false;
public HttpWebHandlerAdapter(WebHandler delegate) {
super(delegate);
@@ -126,15 +129,24 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
* @param codecConfigurer the codec configurer to use
*/
public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) {
Assert.notNull(codecConfigurer, "ServerCodecConfigurer must not be null");
Assert.notNull(codecConfigurer, "ServerCodecConfigurer is required");
this.codecConfigurer = codecConfigurer;
this.disableLoggingRequestDetails = false;
this.codecConfigurer.getReaders().stream()
.filter(LoggingCodecSupport.class::isInstance)
.forEach(reader -> {
if (((LoggingCodecSupport) reader).isDisableLoggingRequestDetails()) {
this.disableLoggingRequestDetails = true;
}
});
}
/**
* Return the configured {@link ServerCodecConfigurer}.
*/
public ServerCodecConfigurer getCodecConfigurer() {
return (this.codecConfigurer != null ? this.codecConfigurer : ServerCodecConfigurer.create());
return this.codecConfigurer;
}
/**
@@ -142,18 +154,18 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
* each created {@link DefaultServerWebExchange}.
* <p>By default this is set to
* {@link org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver}.
* @param localeContextResolver the locale context resolver to use
* @param resolver the locale context resolver to use
*/
public void setLocaleContextResolver(LocaleContextResolver localeContextResolver) {
this.localeContextResolver = localeContextResolver;
public void setLocaleContextResolver(LocaleContextResolver resolver) {
Assert.notNull(resolver, "LocaleContextResolver is required");
this.localeContextResolver = resolver;
}
/**
* Return the configured {@link LocaleContextResolver}.
*/
public LocaleContextResolver getLocaleContextResolver() {
return (this.localeContextResolver != null ?
this.localeContextResolver : new AcceptHeaderLocaleContextResolver());
return this.localeContextResolver;
}
/**
@@ -177,12 +189,36 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
return this.applicationContext;
}
/**
* This method must be invoked after all properties have been set to
* complete initialization.
*/
public void afterPropertiesSet() {
if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
if (this.disableLoggingRequestDetails) {
logger.debug("Logging query, form data, multipart data, and headers is OFF.");
}
else {
logger.warn("\n\n" +
"!!!!!!!!!!!!!!!!!!!\n" +
"Logging query, form and multipart data (DEBUG), and headers (TRACE) may show sensitive data.\n" +
"If not in development, set \"disableLoggingRequestDetails(true)\" on ServerCodecConfigurer,\n" +
"or lower the log level.\n" +
"!!!!!!!!!!!!!!!!!!!\n");
}
}
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
ServerWebExchange exchange = createExchange(request, response);
logExchange(exchange);
return getDelegate().handle(exchange)
.onErrorResume(ex -> handleFailure(request, response, ex))
.doOnSuccess(aVoid -> logResponse(response))
.onErrorResume(ex -> handleUnresolvedError(request, response, ex))
.then(Mono.defer(response::setComplete));
}
@@ -191,27 +227,65 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
getCodecConfigurer(), getLocaleContextResolver(), this.applicationContext);
}
private Mono<Void> handleFailure(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) {
private void logExchange(ServerWebExchange exchange) {
if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
ServerHttpRequest request = exchange.getRequest();
if (logger.isTraceEnabled()) {
String headers = this.disableLoggingRequestDetails ? "" : ", headers=" + request.getHeaders();
logger.trace(formatRequest(request) + headers);
}
else {
logger.debug(formatRequest(request));
}
}
}
private String formatRequest(ServerHttpRequest request) {
String query = "";
if (!this.disableLoggingRequestDetails) {
String rawQuery = request.getURI().getRawQuery();
query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
}
return "HTTP " + request.getMethod() + " " + request.getPath() + query;
}
private void logResponse(ServerHttpResponse response) {
if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
HttpStatus status = response.getStatusCode();
String message = "Completed " + (status != null ? status : "200 OK");
if (logger.isTraceEnabled()) {
String headers = this.disableLoggingRequestDetails ? "" : ", headers=" + response.getHeaders();
logger.trace(message + headers);
}
else {
logger.debug(message);
}
}
}
private Mono<Void> handleUnresolvedError(ServerHttpRequest request, ServerHttpResponse response, Throwable ex) {
if (isDisconnectedClientError(ex)) {
if (disconnectedClientLogger.isTraceEnabled()) {
disconnectedClientLogger.trace("Looks like the client has gone away", ex);
if (lostClientLogger.isTraceEnabled()) {
lostClientLogger.trace("Client went away", ex);
}
else if (disconnectedClientLogger.isDebugEnabled()) {
disconnectedClientLogger.debug("Looks like the client has gone away: " + ex +
" (For a full stack trace, set the log category '" + DISCONNECTED_CLIENT_LOG_CATEGORY +
"' to TRACE level.)");
else if (lostClientLogger.isDebugEnabled()) {
lostClientLogger.debug("Client went away: " + ex +
" (stacktrace at TRACE level for '" + DISCONNECTED_CLIENT_LOG_CATEGORY + "')");
}
return Mono.empty();
}
if (response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR)) {
logger.error("Failed to handle request [" + request.getMethod() + " "
+ request.getURI() + "]", ex);
else if (response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR)) {
logger.error("500 Server Error for " + formatRequest(request), ex);
return Mono.empty();
}
// After the response is committed, propagate errors to the server..
HttpStatus status = response.getStatusCode();
logger.error("Unhandled failure: " + ex.getMessage() + ", response already set (status=" + status + ")");
return Mono.error(ex);
else {
// After the response is committed, propagate errors to the server..
logger.error("Error [" + ex + "] for " + formatRequest(request) +
", but ServerHttpResponse already committed (" + response.getStatusCode() + ")");
return Mono.error(ex);
}
}
private boolean isDisconnectedClientError(Throwable ex) {

View File

@@ -274,11 +274,11 @@ public class WebHttpHandlerBuilder {
public HttpHandler build() {
WebHandler decorated;
decorated = new FilteringWebHandler(this.webHandler, this.filters);
decorated = new ExceptionHandlingWebHandler(decorated, this.exceptionHandlers);
HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
if (this.sessionManager != null) {
adapted.setSessionManager(this.sessionManager);
}
@@ -292,6 +292,8 @@ public class WebHttpHandlerBuilder {
adapted.setApplicationContext(this.applicationContext);
}
adapted.afterPropertiesSet();
return adapted;
}

View File

@@ -30,6 +30,10 @@ import org.springframework.web.server.WebExceptionHandler;
/**
* Handle {@link ResponseStatusException} by setting the response status.
*
* <p>By default exception stack traces are not shown for successfully resolved
* exceptions. Use {@link #setWarnLogCategory(String)} to enable logging with
* stack traces.
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @since 5.0
@@ -39,26 +43,48 @@ public class ResponseStatusExceptionHandler implements WebExceptionHandler {
private static final Log logger = LogFactory.getLog(ResponseStatusExceptionHandler.class);
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
HttpStatus status = resolveStatus(ex);
if (status != null && exchange.getResponse().setStatusCode(status)) {
if (status.is5xxServerError()) {
logger.error(buildMessage(exchange.getRequest(), ex));
}
else if (status == HttpStatus.BAD_REQUEST) {
logger.warn(buildMessage(exchange.getRequest(), ex));
}
else {
logger.trace(buildMessage(exchange.getRequest(), ex));
}
return exchange.getResponse().setComplete();
}
return Mono.error(ex);
@Nullable
private Log warnLogger;
/**
* Set the log category for warn logging.
* <p>Default is no warn logging. Specify this setting to activate warn
* logging into a specific category.
* @see org.apache.commons.logging.LogFactory#getLog(String)
* @see java.util.logging.Logger#getLogger(String)
* @see 5.1
*/
public void setWarnLogCategory(String loggerName) {
this.warnLogger = LogFactory.getLog(loggerName);
}
private String buildMessage(ServerHttpRequest request, Throwable ex) {
return "Failed to handle request [" + request.getMethod() + " " + request.getURI() + "]: " + ex.getMessage();
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
HttpStatus status = resolveStatus(ex);
if (status == null || !exchange.getResponse().setStatusCode(status)) {
return Mono.error(ex);
}
// Mirrors AbstractHandlerExceptionResolver in spring-webmvc..
if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) {
this.warnLogger.warn(formatError(ex, exchange.getRequest()), ex);
}
else if (logger.isDebugEnabled()) {
logger.debug(formatError(ex, exchange.getRequest()));
}
return exchange.getResponse().setComplete();
}
private String formatError(Throwable ex, ServerHttpRequest request) {
String reason = ex.getClass().getSimpleName() + ": " + ex.getMessage();
String path = request.getURI().getRawPath();
return "Resolved [" + reason + "] for HTTP " + request.getMethod() + " " + path;
}
@Nullable

View File

@@ -195,8 +195,8 @@ public class CookieGenerator {
cookie.setHttpOnly(true);
}
response.addCookie(cookie);
if (logger.isDebugEnabled()) {
logger.debug("Added cookie with name [" + getCookieName() + "] and value [" + cookieValue + "]");
if (logger.isTraceEnabled()) {
logger.trace("Added cookie [" + getCookieName() + "=" + cookieValue + "]");
}
}
@@ -220,8 +220,8 @@ public class CookieGenerator {
cookie.setHttpOnly(true);
}
response.addCookie(cookie);
if (logger.isDebugEnabled()) {
logger.debug("Removed cookie with name [" + getCookieName() + "]");
if (logger.isTraceEnabled()) {
logger.trace("Removed cookie '" + getCookieName() + "'");
}
}