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

@@ -33,10 +33,13 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* Central dispatcher for HTTP request handlers/controllers. Dispatches to
@@ -146,10 +149,6 @@ public class DispatcherHandler implements WebHandler, ApplicationContextAware {
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
ServerHttpRequest request = exchange.getRequest();
logger.debug("Processing " + request.getMethodValue() + " request for [" + request.getURI() + "]");
}
if (this.handlerMappings == null) {
return Mono.error(HANDLER_NOT_FOUND_EXCEPTION);
}

View File

@@ -19,9 +19,6 @@ package org.springframework.web.reactive.accept;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
@@ -36,9 +33,6 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class FixedContentTypeResolver implements RequestedContentTypeResolver {
private static final Log logger = LogFactory.getLog(FixedContentTypeResolver.class);
private final List<MediaType> contentTypes;
@@ -71,9 +65,6 @@ public class FixedContentTypeResolver implements RequestedContentTypeResolver {
@Override
public List<MediaType> resolveMediaTypes(ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
logger.debug("Requested media types: " + this.contentTypes);
}
return this.contentTypes;
}

View File

@@ -17,6 +17,7 @@
package org.springframework.web.reactive.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.cache.Cache;
@@ -39,7 +40,7 @@ public class ResourceHandlerRegistration {
private final String[] pathPatterns;
private final List<Resource> locations = new ArrayList<>();
private final List<String> locationValues = new ArrayList<>();
@Nullable
private CacheControl cacheControl;
@@ -77,9 +78,7 @@ public class ResourceHandlerRegistration {
* chained method invocation
*/
public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) {
for (String location : resourceLocations) {
this.locations.add(this.resourceLoader.getResource(location));
}
this.locationValues.addAll(Arrays.asList(resourceLocations));
return this;
}
@@ -145,11 +144,12 @@ public class ResourceHandlerRegistration {
*/
protected ResourceWebHandler getRequestHandler() {
ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocationValues(this.locationValues);
handler.setResourceLoader(this.resourceLoader);
if (this.resourceChainRegistration != null) {
handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
}
handler.setLocations(this.locations);
if (this.cacheControl != null) {
handler.setCacheControl(this.cacheControl);
}

View File

@@ -25,6 +25,8 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.codec.LoggingCodecSupport;
import org.springframework.util.Assert;
/**
@@ -69,12 +71,22 @@ public abstract class ExchangeFunctions {
private final ExchangeStrategies strategies;
private boolean disableLoggingRequestDetails;
public DefaultExchangeFunction(ClientHttpConnector connector, ExchangeStrategies strategies) {
Assert.notNull(connector, "ClientHttpConnector must not be null");
Assert.notNull(strategies, "ExchangeStrategies must not be null");
this.connector = connector;
this.strategies = strategies;
strategies.messageWriters().stream()
.filter(LoggingCodecSupport.class::isInstance)
.forEach(reader -> {
if (((LoggingCodecSupport) reader).isDisableLoggingRequestDetails()) {
this.disableLoggingRequestDetails = true;
}
});
}
@@ -87,19 +99,39 @@ public abstract class ExchangeFunctions {
return this.connector
.connect(httpMethod, url, httpRequest -> request.writeTo(httpRequest, this.strategies))
.doOnSubscribe(subscription -> logger.debug("Subscriber present"))
.doOnRequest(n -> logger.debug("Demand signaled"))
.doOnCancel(() -> logger.debug("Cancelling request"))
.doOnRequest(n -> logRequest(request))
.doOnCancel(() -> logger.debug("Cancel signal (to close connection)"))
.map(response -> {
if (logger.isDebugEnabled()) {
int code = response.getRawStatusCode();
HttpStatus status = HttpStatus.resolve(code);
String reason = status != null ? " " + status.getReasonPhrase() : "";
logger.debug("Response received, status: " + code + reason);
}
logResponse(response);
return new DefaultClientResponse(response, this.strategies);
});
}
private void logRequest(ClientRequest request) {
if (logger.isDebugEnabled()) {
String formatted = request.url().toString();
if (this.disableLoggingRequestDetails) {
int index = formatted.indexOf("?");
formatted = index != -1 ? formatted.substring(0, index) : formatted;
}
logger.debug("HTTP " + request.method() + " " + formatted);
}
}
private void logResponse(ClientHttpResponse response) {
if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
int code = response.getRawStatusCode();
HttpStatus status = HttpStatus.resolve(code);
String message = "Response " + (status != null ? status : code);
if (logger.isTraceEnabled()) {
String headers = this.disableLoggingRequestDetails ? "" : ", headers=" + response.getHeaders();
logger.trace(message + headers);
}
else {
logger.debug(message);
}
}
}
}
}

View File

@@ -216,7 +216,7 @@ class DefaultServerRequest implements ServerRequest {
@Override
public String toString() {
return String.format("%s %s", method(), path());
return String.format("HTTP %s %s", method(), path());
}

View File

@@ -328,9 +328,8 @@ public abstract class RequestPredicates {
private static void traceMatch(String prefix, Object desired, @Nullable Object actual, boolean match) {
if (logger.isTraceEnabled()) {
String message = String.format("%s \"%s\" %s against value \"%s\"",
prefix, desired, match ? "matches" : "does not match", actual);
logger.trace(message);
logger.trace(String.format("%s \"%s\" %s against value \"%s\"",
prefix, desired, match ? "matches" : "does not match", actual));
}
}

View File

@@ -421,8 +421,8 @@ public abstract class RouterFunctions {
@Override
public Mono<HandlerFunction<T>> route(ServerRequest request) {
if (this.predicate.test(request)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("Predicate \"%s\" matches against \"%s\"", this.predicate, request));
if (logger.isTraceEnabled()) {
logger.trace(String.format("Matched %s", this.predicate));
}
return Mono.just(this.handlerFunction);
}
@@ -456,19 +456,15 @@ public abstract class RouterFunctions {
public Mono<HandlerFunction<T>> route(ServerRequest serverRequest) {
return this.predicate.nest(serverRequest)
.map(nestedRequest -> {
if (logger.isDebugEnabled()) {
logger.debug(
String.format(
"Nested predicate \"%s\" matches against \"%s\"",
this.predicate, serverRequest));
if (logger.isTraceEnabled()) {
logger.trace(String.format("Matched nested %s", this.predicate));
}
return this.routerFunction.route(nestedRequest)
.doOnNext(match -> {
mergeTemplateVariables(serverRequest, nestedRequest.pathVariables());
});
}
)
.orElseGet(Mono::empty);
).orElseGet(Mono::empty);
}
@SuppressWarnings("unchecked")

View File

@@ -105,28 +105,37 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
* Initialized the router functions by detecting them in the application context.
*/
protected void initRouterFunctions() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for router functions in application context: " +
getApplicationContext());
}
List<RouterFunction<?>> routerFunctions = routerFunctions();
if (!CollectionUtils.isEmpty(routerFunctions) && logger.isInfoEnabled()) {
routerFunctions.forEach(routerFunction -> logger.info("Mapped " + routerFunction));
}
this.routerFunction = routerFunctions.stream()
.reduce(RouterFunction::andOther)
.orElse(null);
this.routerFunction = routerFunctions.stream().reduce(RouterFunction::andOther).orElse(null);
logRouterFunctions(routerFunctions);
}
private List<RouterFunction<?>> routerFunctions() {
SortedRouterFunctionsContainer container = new SortedRouterFunctionsContainer();
obtainApplicationContext().getAutowireCapableBeanFactory().autowireBean(container);
return CollectionUtils.isEmpty(container.routerFunctions) ? Collections.emptyList() :
container.routerFunctions;
List<RouterFunction<?>> functions = container.routerFunctions;
return CollectionUtils.isEmpty(functions) ? Collections.emptyList() : functions;
}
private void logRouterFunctions(List<RouterFunction<?>> routerFunctions) {
if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
int total = routerFunctions.size();
String message = total + " RouterFunction(s) in " + formatMappingName();
if (logger.isTraceEnabled()) {
if (total > 0) {
routerFunctions.forEach(routerFunction -> logger.trace("Mapped " + routerFunction));
}
else {
logger.trace(message);
}
}
else if (total > 0) {
logger.debug(message);
}
}
}
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
if (this.routerFunction != null) {

View File

@@ -20,6 +20,7 @@ import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.support.ApplicationObjectSupport;
import org.springframework.core.Ordered;
import org.springframework.lang.Nullable;
@@ -44,7 +45,8 @@ import org.springframework.web.util.pattern.PathPatternParser;
* @author Brian Clozel
* @since 5.0
*/
public abstract class AbstractHandlerMapping extends ApplicationObjectSupport implements HandlerMapping, Ordered {
public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
implements HandlerMapping, Ordered, BeanNameAware {
private static final WebHandler REQUEST_HANDLED_HANDLER = exchange -> Mono.empty();
@@ -57,6 +59,9 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport im
private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
@Nullable
private String beanName;
public AbstractHandlerMapping() {
this.patternParser = new PathPatternParser();
@@ -141,10 +146,22 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport im
return this.order;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
protected String formatMappingName() {
return this.beanName != null ? "'" + this.beanName + "'" : "<unknown>";
}
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
return getHandlerInternal(exchange).map(handler -> {
if (logger.isDebugEnabled()) {
logger.debug("Mapped to " + handler);
}
if (CorsUtils.isCorsRequest(exchange.getRequest())) {
CorsConfiguration configA = this.globalCorsConfigSource.getCorsConfiguration(exchange);
CorsConfiguration configB = getCorsConfiguration(handler, exchange);

View File

@@ -18,7 +18,9 @@ package org.springframework.web.reactive.handler;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import reactor.core.publisher.Mono;
@@ -90,14 +92,6 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
catch (Exception ex) {
return Mono.error(ex);
}
if (handler != null && logger.isDebugEnabled()) {
logger.debug("Mapping [" + lookupPath + "] to " + handler);
}
else if (handler == null && logger.isTraceEnabled()) {
logger.trace("No handler mapping found for [" + lookupPath + "]");
}
return Mono.justOrEmpty(handler);
}
@@ -113,20 +107,25 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
*/
@Nullable
protected Object lookupHandler(PathContainer lookupPath, ServerWebExchange exchange) throws Exception {
return this.handlerMap.entrySet().stream()
.filter(entry -> entry.getKey().matches(lookupPath))
.sorted((entry1, entry2) ->
PathPattern.SPECIFICITY_COMPARATOR.compare(entry1.getKey(), entry2.getKey()))
.findFirst()
.map(entry -> {
PathPattern pattern = entry.getKey();
if (logger.isDebugEnabled()) {
logger.debug("Matching pattern for request [" + lookupPath + "] is " + pattern);
}
PathContainer pathWithinMapping = pattern.extractPathWithinPattern(lookupPath);
return handleMatch(entry.getValue(), pattern, pathWithinMapping, exchange);
})
.orElse(null);
List<PathPattern> matches = this.handlerMap.keySet().stream()
.filter(key -> key.matches(lookupPath))
.collect(Collectors.toList());
if (matches.isEmpty()) {
return null;
}
if (matches.size() > 1) {
matches.sort(PathPattern.SPECIFICITY_COMPARATOR);
if (logger.isTraceEnabled()) {
logger.debug("Matching patterns " + matches);
}
}
PathPattern pattern = matches.get(0);
PathContainer pathWithinMapping = pattern.extractPathWithinPattern(lookupPath);
return handleMatch(this.handlerMap.get(pattern), pattern, pathWithinMapping, exchange);
}
private Object handleMatch(Object handler, PathPattern bestMatch, PathContainer pathWithinMapping,
@@ -207,14 +206,13 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
// Register resolved handler
this.handlerMap.put(pattern, resolvedHandler);
if (logger.isInfoEnabled()) {
logger.info("Mapped URL path [" + urlPath + "] onto " + getHandlerDescription(handler));
if (logger.isTraceEnabled()) {
logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
private String getHandlerDescription(Object handler) {
return "handler " + (handler instanceof String ?
"'" + handler + "'" : "of type [" + handler.getClass() + "]");
return (handler instanceof String ? "'" + handler + "'" : handler.toString());
}

View File

@@ -16,7 +16,9 @@
package org.springframework.web.reactive.handler;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -110,7 +112,7 @@ public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
*/
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
if (urlMap.isEmpty()) {
logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
logger.trace("No patterns in " + formatMappingName());
}
else {
for (Map.Entry<String, Object> entry : urlMap.entrySet()) {
@@ -126,6 +128,9 @@ public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
}
registerHandler(url, handler);
}
if (logger.isDebugEnabled()) {
logger.debug("Patterns " + getHandlerMap().keySet() + " in " + formatMappingName());
}
}
}

View File

@@ -41,9 +41,6 @@ public abstract class AbstractResourceResolver implements ResourceResolver {
public Mono<Resource> resolveResource(@Nullable ServerWebExchange exchange, String requestPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
if (logger.isTraceEnabled()) {
logger.trace("Resolving resource for request path \"" + requestPath + "\"");
}
return resolveResourceInternal(exchange, requestPath, locations, chain);
}
@@ -51,10 +48,6 @@ public abstract class AbstractResourceResolver implements ResourceResolver {
public Mono<String> resolveUrlPath(String resourceUrlPath, List<? extends Resource> locations,
ResourceResolverChain chain) {
if (logger.isTraceEnabled()) {
logger.trace("Resolving public URL for resource path \"" + resourceUrlPath + "\"");
}
return resolveUrlPathInternal(resourceUrlPath, locations, chain);
}

View File

@@ -127,13 +127,10 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport {
if (!content.startsWith(MANIFEST_HEADER)) {
if (logger.isTraceEnabled()) {
logger.trace("Manifest should start with 'CACHE MANIFEST', skip: " + resource);
logger.trace("Skipping " + resource + ": Manifest does not start with 'CACHE MANIFEST'");
}
return Mono.just(resource);
}
if (logger.isTraceEnabled()) {
logger.trace("Transforming resource: " + resource);
}
return Flux.generate(new LineInfoGenerator(content))
.concatMap(info -> processLine(info, exchange, resource, chain))
.reduce(new ByteArrayOutputStream(), (out, line) -> {
@@ -143,9 +140,6 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport {
.map(out -> {
String hash = DigestUtils.md5DigestAsHex(out.toByteArray());
writeToByteArrayOutputStream(out, "\n" + "# Hash: " + hash);
if (logger.isTraceEnabled()) {
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
}
return new TransformedResource(resource, out.toByteArray());
});
}
@@ -168,12 +162,7 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport {
}
String link = toAbsolutePath(info.getLine(), exchange);
return resolveUrlPath(link, exchange, resource, chain)
.doOnNext(path -> {
if (logger.isTraceEnabled()) {
logger.trace("Link modified: " + path + " (original: " + info.getLine() + ")");
}
});
return resolveUrlPath(link, exchange, resource, chain);
}

View File

@@ -109,19 +109,12 @@ public class CachingResourceResolver extends AbstractResourceResolver {
Resource cachedResource = this.cache.get(key, Resource.class);
if (cachedResource != null) {
if (logger.isTraceEnabled()) {
logger.trace("Found match: " + cachedResource);
}
logger.trace("Resource resolved from cache");
return Mono.just(cachedResource);
}
return chain.resolveResource(exchange, requestPath, locations)
.doOnNext(resource -> {
if (logger.isTraceEnabled()) {
logger.trace("Putting resolved resource in cache: " + resource);
}
this.cache.put(key, resource);
});
.doOnNext(resource -> this.cache.put(key, resource));
}
protected String computeKey(@Nullable ServerWebExchange exchange, String requestPath) {
@@ -160,19 +153,12 @@ public class CachingResourceResolver extends AbstractResourceResolver {
String cachedUrlPath = this.cache.get(key, String.class);
if (cachedUrlPath != null) {
if (logger.isTraceEnabled()) {
logger.trace("Found match: \"" + cachedUrlPath + "\"");
}
logger.trace("Path resolved from cache");
return Mono.just(cachedUrlPath);
}
return chain.resolveUrlPath(resourceUrlPath, locations)
.doOnNext(resolvedPath -> {
if (logger.isTraceEnabled()) {
logger.trace("Putting resolved resource URL path in cache: \"" + resolvedPath + "\"");
}
this.cache.put(key, resolvedPath);
});
.doOnNext(resolvedPath -> this.cache.put(key, resolvedPath));
}
}

View File

@@ -69,19 +69,12 @@ public class CachingResourceTransformer implements ResourceTransformer {
Resource cachedResource = this.cache.get(resource, Resource.class);
if (cachedResource != null) {
if (logger.isTraceEnabled()) {
logger.trace("Found match: " + cachedResource);
}
logger.trace("Resource resolved from cache");
return Mono.just(cachedResource);
}
return transformerChain.transform(exchange, resource)
.doOnNext(transformed -> {
if (logger.isTraceEnabled()) {
logger.trace("Putting transformed resource in cache: " + transformed);
}
this.cache.put(resource, transformed);
});
.doOnNext(transformed -> this.cache.put(resource, transformed));
}
}

View File

@@ -84,10 +84,6 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport {
return Mono.just(ouptputResource);
}
if (logger.isTraceEnabled()) {
logger.trace("Transforming resource: " + ouptputResource);
}
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
Flux<DataBuffer> flux = DataBufferUtils
.read(ouptputResource, bufferFactory, StreamUtils.BUFFER_SIZE);
@@ -106,9 +102,6 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport {
List<ContentChunkInfo> contentChunkInfos = parseContent(cssContent);
if (contentChunkInfos.isEmpty()) {
if (logger.isTraceEnabled()) {
logger.trace("No links found.");
}
return Mono.just(resource);
}
@@ -228,8 +221,8 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport {
if (content.substring(position, position + 4).equals("url(")) {
// Ignore, UrlFunctionContentParser will take care
}
else if (logger.isErrorEnabled()) {
logger.error("Unexpected syntax for @import link at index " + position);
else if (logger.isTraceEnabled()) {
logger.trace("Unexpected syntax for @import link at index " + position);
}
return position;
}

View File

@@ -59,7 +59,7 @@ public class GzipResourceResolver extends AbstractResourceResolver {
}
}
catch (IOException ex) {
logger.trace("No gzipped resource for [" + resource.getFilename() + "]", ex);
logger.trace("No gzip resource for [" + resource.getFilename() + "]", ex);
}
}
return resource;

View File

@@ -111,27 +111,27 @@ public class PathResourceResolver extends AbstractResourceResolver {
Resource resource = location.createRelative(resourcePath);
if (resource.isReadable()) {
if (checkResource(resource, location)) {
if (logger.isTraceEnabled()) {
logger.trace("Found match: " + resource);
}
return Mono.just(resource);
}
else if (logger.isTraceEnabled()) {
else if (logger.isWarnEnabled()) {
Resource[] allowedLocations = getAllowedLocations();
logger.trace("Resource path \"" + resourcePath + "\" was successfully resolved " +
logger.warn("Resource path \"" + resourcePath + "\" was successfully resolved " +
"but resource \"" + resource.getURL() + "\" is neither under the " +
"current location \"" + location.getURL() + "\" nor under any of the " +
"allowed locations " + (allowedLocations != null ? Arrays.asList(allowedLocations) : "[]"));
}
}
else if (logger.isTraceEnabled()) {
logger.trace("No match for location: " + location);
}
return Mono.empty();
}
catch (IOException ex) {
if (logger.isTraceEnabled()) {
logger.trace("Failure checking for relative resource under location + " + location, ex);
if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
String error = "Skip location [" + location + "] due to error";
if (logger.isTraceEnabled()) {
logger.trace(error, ex);
}
else {
logger.debug(error + ": " + ex.getMessage());
}
}
return Mono.error(ex);
}
@@ -194,9 +194,7 @@ public class PathResourceResolver extends AbstractResourceResolver {
try {
String decodedPath = URLDecoder.decode(resourcePath, "UTF-8");
if (decodedPath.contains("../") || decodedPath.contains("..\\")) {
if (logger.isTraceEnabled()) {
logger.trace("Resolved resource path contains encoded \"../\" or \"..\\\": " + resourcePath);
}
logger.warn("Resolved resource path contains encoded \"../\" or \"..\\\": " + resourcePath);
return true;
}
}

View File

@@ -87,15 +87,10 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
public void onApplicationEvent(ContextRefreshedEvent event) {
if (this.handlerMap.isEmpty()) {
detectResourceHandlers(event.getApplicationContext());
if(logger.isDebugEnabled()) {
logger.debug("No resource handling mappings found");
}
}
}
private void detectResourceHandlers(ApplicationContext context) {
logger.debug("Looking for resource handler mappings");
Map<String, SimpleUrlHandlerMapping> beans = context.getBeansOfType(SimpleUrlHandlerMapping.class);
List<SimpleUrlHandlerMapping> mappings = new ArrayList<>(beans.values());
AnnotationAwareOrderComparator.sort(mappings);
@@ -104,14 +99,13 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
mapping.getHandlerMap().forEach((pattern, handler) -> {
if (handler instanceof ResourceWebHandler) {
ResourceWebHandler resourceHandler = (ResourceWebHandler) handler;
if (logger.isDebugEnabled()) {
logger.debug("Found resource handler mapping: URL pattern=\"" + pattern + "\", " +
"locations=" + resourceHandler.getLocations() + ", " +
"resolvers=" + resourceHandler.getResourceResolvers());
}
this.handlerMap.put(pattern, resourceHandler);
}
}));
if (this.handlerMap.isEmpty()) {
logger.trace("No resource handling mappings found");
}
}
@@ -124,17 +118,11 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
* @return the resolved public resource URL path, or empty if unresolved
*/
public final Mono<String> getForUriString(String uriString, ServerWebExchange exchange) {
if (logger.isTraceEnabled()) {
logger.trace("Getting resource URL for request URL \"" + uriString + "\"");
}
ServerHttpRequest request = exchange.getRequest();
int queryIndex = getQueryIndex(uriString);
String lookupPath = uriString.substring(0, queryIndex);
String query = uriString.substring(queryIndex);
PathContainer parsedLookupPath = PathContainer.parsePath(lookupPath);
if (logger.isTraceEnabled()) {
logger.trace("Getting resource URL for lookup path \"" + lookupPath + "\"");
}
return resolveResourceUrl(parsedLookupPath).map(resolvedPath ->
request.getPath().contextPath().value() + resolvedPath + query);
}
@@ -162,23 +150,21 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
PathContainer path = entry.getKey().extractPathWithinPattern(lookupPath);
int endIndex = lookupPath.elements().size() - path.elements().size();
PathContainer mapping = lookupPath.subPath(0, endIndex);
if (logger.isTraceEnabled()) {
logger.trace("Invoking ResourceResolverChain for URL pattern " +
"\"" + entry.getKey() + "\"");
}
ResourceWebHandler handler = entry.getValue();
List<ResourceResolver> resolvers = handler.getResourceResolvers();
ResourceResolverChain chain = new DefaultResourceResolverChain(resolvers);
return chain.resolveUrlPath(path.value(), handler.getLocations())
.map(resolvedPath -> {
if (logger.isTraceEnabled()) {
logger.trace("Resolved public resource URL path \"" + resolvedPath + "\"");
}
return mapping.value() + resolvedPath;
});
})
.orElse(Mono.empty());
.orElseGet(() ->{
if (logger.isTraceEnabled()) {
logger.trace("No match for \"" + lookupPath + "\"");
}
return Mono.empty();
});
}

View File

@@ -25,6 +25,7 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -33,6 +34,7 @@ import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -90,6 +92,8 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);
private final List<String> locationValues = new ArrayList<>(4);
private final List<Resource> locations = new ArrayList<>(4);
private final List<ResourceResolver> resourceResolvers = new ArrayList<>(4);
@@ -108,6 +112,28 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
@Nullable
private ResourceHttpMessageWriter resourceHttpMessageWriter;
@Nullable
private ResourceLoader resourceLoader;
/**
* Accepts a list of String-based location values to be resolved into
* {@link Resource} locations.
* @since 5.1
*/
public void setLocationValues(List<String> locationValues) {
Assert.notNull(locationValues, "Location values list must not be null");
this.locationValues.clear();
this.locationValues.addAll(locationValues);
}
/**
* Return the configured location values.
* @since 5.1
*/
public List<String> getLocationValues() {
return this.locationValues;
}
/**
* Set the {@code List} of {@code Resource} paths to use as sources
@@ -123,6 +149,11 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
/**
* Return the {@code List} of {@code Resource} paths to use as sources
* for serving static resources.
* <p>Note that if {@link #setLocationValues(List) locationValues} are provided,
* instead of loaded Resource-based locations, this method will return
* empty until after initialization via {@link #afterPropertiesSet()}.
* @see #setLocationValues
* @see #setLocations
*/
public List<Resource> getLocations() {
return this.locations;
@@ -198,9 +229,25 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
return this.resourceHttpMessageWriter;
}
/**
* Provide the ResourceLoader to load {@link #setLocationValues(List)
* location values} with.
* @since 5.1
*/
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
public void afterPropertiesSet() throws Exception {
resolveResourceLocations();
if (logger.isWarnEnabled() && CollectionUtils.isEmpty(this.locations)) {
logger.warn("Locations list is empty. No resources will be served unless a " +
"custom ResourceResolver is configured as an alternative to PathResourceResolver.");
}
if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}
@@ -216,6 +263,24 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
this.transformerChain = new DefaultResourceTransformerChain(this.resolverChain, this.resourceTransformers);
}
private void resolveResourceLocations() {
if (CollectionUtils.isEmpty(this.locationValues)) {
return;
}
else if (!CollectionUtils.isEmpty(this.locations)) {
throw new IllegalArgumentException("Please set either Resource-based \"locations\" or " +
"String-based \"locationValues\", but not both.");
}
Assert.notNull(this.resourceLoader,
"ResourceLoader is required when \"locationValues\" are configured.");
for (String location : this.locationValues) {
Resource resource = this.resourceLoader.getResource(location);
this.locations.add(resource);
}
}
/**
* Look for a {@code PathResourceResolver} among the configured resource
* resolvers and set its {@code allowedLocations} property (if empty) to
@@ -257,7 +322,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
public Mono<Void> handle(ServerWebExchange exchange) {
return getResource(exchange)
.switchIfEmpty(Mono.defer(() -> {
logger.trace("No matching resource found - returning 404");
logger.debug("Resource not found");
return Mono.error(NOT_FOUND_EXCEPTION);
}))
.flatMap(resource -> {
@@ -276,7 +341,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
// Header phase
if (exchange.checkNotModified(Instant.ofEpochMilli(resource.lastModified()))) {
logger.trace("Resource not modified - returning 304");
logger.trace("Resource not modified");
return Mono.empty();
}
@@ -290,23 +355,11 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
// Check the media type for the resource
MediaType mediaType = MediaTypeFactory.getMediaType(resource).orElse(null);
if (mediaType != null) {
if (logger.isTraceEnabled()) {
logger.trace("Determined media type '" + mediaType + "' for " + resource);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No media type found " +
"for " + resource + " - not sending a content-type header");
}
}
// Content phase
if (HttpMethod.HEAD.matches(exchange.getRequest().getMethodValue())) {
setHeaders(exchange, resource, mediaType);
exchange.getResponse().getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
logger.trace("HEAD request - skipping content");
return Mono.empty();
}
@@ -329,15 +382,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
String path = processPath(pathWithinHandler.value());
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
if (logger.isTraceEnabled()) {
logger.trace("Ignoring invalid resource path [" + path + "]");
}
return Mono.empty();
}
if (isInvalidEncodedPath(path)) {
if (logger.isTraceEnabled()) {
logger.trace("Ignoring invalid resource path with escape sequences [" + path + "]");
}
return Mono.empty();
}
@@ -399,11 +446,7 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
if (i == 0 || (i == 1 && slash)) {
return path;
}
path = slash ? "/" + path.substring(i) : path.substring(i);
if (logger.isTraceEnabled()) {
logger.trace("Path after trimming leading '/' and control characters: " + path);
}
return path;
return slash ? "/" + path.substring(i) : path.substring(i);
}
}
return (slash ? "/" : "");
@@ -450,30 +493,21 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
* @return {@code true} if the path is invalid, {@code false} otherwise
*/
protected boolean isInvalidPath(String path) {
if (logger.isTraceEnabled()) {
logger.trace("Applying \"invalid path\" checks to path: " + path);
}
if (path.contains("WEB-INF") || path.contains("META-INF")) {
if (logger.isTraceEnabled()) {
logger.trace("Path contains \"WEB-INF\" or \"META-INF\".");
}
logger.warn("Path contains \"WEB-INF\" or \"META-INF\".");
return true;
}
if (path.contains(":/")) {
String relativePath = (path.charAt(0) == '/' ? path.substring(1) : path);
if (ResourceUtils.isUrl(relativePath) || relativePath.startsWith("url:")) {
if (logger.isTraceEnabled()) {
logger.trace("Path represents URL or has \"url:\" prefix.");
}
logger.warn("Path represents URL or has \"url:\" prefix.");
return true;
}
}
if (path.contains("..")) {
path = StringUtils.cleanPath(path);
if (path.contains("../")) {
if (logger.isTraceEnabled()) {
logger.trace("Path contains \"../\" after call to StringUtils#cleanPath.");
}
logger.warn("Path contains \"../\" after call to StringUtils#cleanPath.");
return true;
}
}
@@ -506,7 +540,16 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
@Override
public String toString() {
return "ResourceWebHandler [locations=" + getLocations() + ", resolvers=" + getResourceResolvers() + "]";
return "ResourceWebHandler " + formatLocations();
}
private Object formatLocations() {
if (!this.locationValues.isEmpty()) {
return this.locationValues.stream().collect(Collectors.joining("\", \"", "[\"", "\"]"));
}
else if (!this.locations.isEmpty()) {
return "[" + this.locations + "]";
}
return Collections.emptyList();
}
}

View File

@@ -173,30 +173,20 @@ public class VersionResourceResolver extends AbstractResourceResolver {
String candidate = versionStrategy.extractVersion(requestPath);
if (StringUtils.isEmpty(candidate)) {
if (logger.isTraceEnabled()) {
logger.trace("No version found in path \"" + requestPath + "\"");
}
return Mono.empty();
}
String simplePath = versionStrategy.removeVersion(requestPath, candidate);
if (logger.isTraceEnabled()) {
logger.trace("Extracted version from path, re-resolving without version: \"" + simplePath + "\"");
}
return chain.resolveResource(exchange, simplePath, locations)
.filterWhen(resource -> versionStrategy.getResourceVersion(resource)
.map(actual -> {
if (candidate.equals(actual)) {
if (logger.isTraceEnabled()) {
logger.trace("Resource matches extracted version [" + candidate + "]");
}
return true;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Potential resource found for \"" + requestPath + "\", " +
"but version [" + candidate + "] does not match");
logger.trace("Found resource for \"" + requestPath + "\", but version [" +
candidate + "] does not match");
}
return false;
}
@@ -215,16 +205,9 @@ public class VersionResourceResolver extends AbstractResourceResolver {
if (strategy == null) {
return Mono.just(baseUrl);
}
if (logger.isTraceEnabled()) {
logger.trace("Getting the original resource to determine version " +
"for path \"" + resourceUrlPath + "\"");
}
return chain.resolveResource(null, baseUrl, locations)
.flatMap(resource -> strategy.getResourceVersion(resource)
.map(version -> {
if (logger.isTraceEnabled()) {
logger.trace("Determined version [" + version + "] for " + resource);
}
return strategy.addVersion(baseUrl, version);
}));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -18,7 +18,6 @@ package org.springframework.web.reactive.resource;
import java.util.List;
import org.webjars.MultipleMatchesException;
import org.webjars.WebJarAssetLocator;
import reactor.core.publisher.Mono;
@@ -105,26 +104,16 @@ public class WebJarsResourceResolver extends AbstractResourceResolver {
@Nullable
protected String findWebJarResourcePath(String path) {
try {
int startOffset = (path.startsWith("/") ? 1 : 0);
int endOffset = path.indexOf('/', 1);
if (endOffset != -1) {
String webjar = path.substring(startOffset, endOffset);
String partialPath = path.substring(endOffset);
String webJarPath = webJarAssetLocator.getFullPath(webjar, partialPath);
int startOffset = (path.startsWith("/") ? 1 : 0);
int endOffset = path.indexOf('/', 1);
if (endOffset != -1) {
String webjar = path.substring(startOffset, endOffset);
String partialPath = path.substring(endOffset + 1);
String webJarPath = webJarAssetLocator.getFullPathExact(webjar, partialPath);
if (webJarPath != null) {
return webJarPath.substring(WEBJARS_LOCATION_LENGTH);
}
}
catch (MultipleMatchesException ex) {
if (logger.isWarnEnabled()) {
logger.warn("WebJar version conflict for \"" + path + "\"", ex);
}
}
catch (IllegalArgumentException ex) {
if (logger.isTraceEnabled()) {
logger.trace("No WebJar resource found for \"" + path + "\"");
}
}
return null;
}

View File

@@ -23,6 +23,9 @@ import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
@@ -47,6 +50,8 @@ public abstract class HandlerResultHandlerSupport implements Ordered {
private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application");
protected final Log logger = LogFactory.getLog(getClass());
private final RequestedContentTypeResolver contentTypeResolver;
private final ReactiveAdapterRegistry adapterRegistry;
@@ -117,6 +122,9 @@ public abstract class HandlerResultHandlerSupport implements Ordered {
MediaType contentType = exchange.getResponse().getHeaders().getContentType();
if (contentType != null && contentType.isConcrete()) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
return contentType;
}
@@ -137,13 +145,24 @@ public abstract class HandlerResultHandlerSupport implements Ordered {
for (MediaType mediaType : result) {
if (mediaType.isConcrete()) {
if (logger.isDebugEnabled()) {
logger.debug("Using '" + mediaType + "' given " + acceptableTypes);
}
return mediaType;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION_ALL)) {
return MediaType.APPLICATION_OCTET_STREAM;
mediaType = MediaType.APPLICATION_OCTET_STREAM;
if (logger.isDebugEnabled()) {
logger.debug("Using '" + mediaType + "' given " + acceptableTypes);
}
return mediaType;
}
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return null;
}

View File

@@ -34,6 +34,7 @@ import reactor.core.publisher.Mono;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.MethodIntrospector;
import org.springframework.http.server.RequestPath;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -122,6 +123,9 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
* @param method the method
*/
public void registerMapping(T mapping, Object handler, Method method) {
if (logger.isTraceEnabled()) {
logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
}
this.mappingRegistry.register(mapping, handler, method);
}
@@ -131,6 +135,9 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
* @param mapping the mapping to unregister
*/
public void unregisterMapping(T mapping) {
if (logger.isTraceEnabled()) {
logger.trace("Unregister mapping \"" + mapping);
}
this.mappingRegistry.unregister(mapping);
}
@@ -142,7 +149,15 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
*/
@Override
public void afterPropertiesSet() {
initHandlerMethods();
// Total includes detected mappings + explicit registrations via registerMapping..
int total = this.getHandlerMethods().size();
if ((logger.isTraceEnabled() && total == 0) || (logger.isDebugEnabled() && total > 0) ) {
logger.debug(total + " mappings in " + formatMappingName());
}
}
/**
@@ -152,9 +167,6 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
* @see #handlerMethodsInitialized(Map)
*/
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " + getApplicationContext());
}
String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class);
for (String beanName : beanNames) {
@@ -165,8 +177,8 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
@@ -189,8 +201,8 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
if (logger.isDebugEnabled()) {
logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
if (logger.isTraceEnabled()) {
logger.trace("Mapped " + methods.size() + " handler method(s) for " + userType + ": " + methods);
}
methods.forEach((key, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(key, userType);
@@ -255,10 +267,6 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
*/
@Override
public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) {
if (logger.isDebugEnabled()) {
logger.debug("Looking up handler method for path " +
exchange.getRequest().getPath().value());
}
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod;
@@ -268,15 +276,6 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
catch (Exception ex) {
return Mono.error(ex);
}
if (logger.isDebugEnabled()) {
if (handlerMethod != null) {
logger.debug("Returning handler method [" + handlerMethod + "]");
}
else {
logger.debug("Did not find handler method for " +
"[" + exchange.getRequest().getPath().value() + "]");
}
}
if (handlerMethod != null) {
handlerMethod = handlerMethod.createWithResolvedBean();
}
@@ -303,12 +302,11 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(exchange));
matches.sort(comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" +
exchange.getRequest().getPath() + "] : " + matches);
}
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
@@ -316,8 +314,9 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
exchange.getRequest().getPath() + "': {" + m1 + ", " + m2 + "}");
RequestPath path = exchange.getRequest().getPath();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + path + "': {" + m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, bestMatch.handlerMethod, exchange);
@@ -464,9 +463,6 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
assertUniqueMethodMapping(handlerMethod, mapping);
if (logger.isInfoEnabled()) {
logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
}
this.mappingLookup.put(mapping, handlerMethod);
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);

View File

@@ -20,6 +20,9 @@ import java.lang.annotation.Annotation;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
@@ -35,6 +38,8 @@ import org.springframework.util.Assert;
*/
public abstract class HandlerMethodArgumentResolverSupport implements HandlerMethodArgumentResolver {
protected final Log logger = LogFactory.getLog(getClass());
private final ReactiveAdapterRegistry adapterRegistry;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -38,9 +38,9 @@ import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.HandlerResult;
@@ -134,31 +134,38 @@ public class InvocableHandlerMethod extends HandlerMethod {
Object... providedArgs) {
return resolveArguments(exchange, bindingContext, providedArgs).flatMap(args -> {
Object value;
try {
Object value = doInvoke(args);
HttpStatus status = getResponseStatus();
if (status != null) {
exchange.getResponse().setStatusCode(status);
}
MethodParameter returnType = getReturnType();
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(returnType.getParameterType());
boolean asyncVoid = isAsyncVoidReturnType(returnType, adapter);
if ((value == null || asyncVoid) && isResponseHandled(args, exchange)) {
logger.debug("Response fully handled in controller method");
return asyncVoid ? Mono.from(adapter.toPublisher(value)) : Mono.empty();
}
HandlerResult result = new HandlerResult(this, value, returnType, bindingContext);
return Mono.just(result);
ReflectionUtils.makeAccessible(getBridgedMethod());
value = getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
throw new IllegalStateException(formatInvokeError(text, args), ex);
}
catch (InvocationTargetException ex) {
return Mono.error(ex.getTargetException());
}
catch (Throwable ex) {
return Mono.error(new IllegalStateException(getInvocationErrorMessage(args)));
// Unlikely to ever get here, but it must be handled...
return Mono.error(new IllegalStateException(formatInvokeError("Invocation failure", args), ex));
}
HttpStatus status = getResponseStatus();
if (status != null) {
exchange.getResponse().setStatusCode(status);
}
MethodParameter returnType = getReturnType();
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(returnType.getParameterType());
boolean asyncVoid = isAsyncVoidReturnType(returnType, adapter);
if ((value == null || asyncVoid) && isResponseHandled(args, exchange)) {
return asyncVoid ? Mono.from(adapter.toPublisher(value)) : Mono.empty();
}
HandlerResult result = new HandlerResult(this, value, returnType, bindingContext);
return Mono.just(result);
});
}
@@ -203,8 +210,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
private HandlerMethodArgumentResolver findResolver(MethodParameter param) {
return this.resolvers.stream()
.filter(r -> r.supportsParameter(param))
.findFirst()
.orElseThrow(() -> getArgumentError("No suitable resolver for", param, null));
.findFirst().orElseThrow(() ->
new IllegalStateException(formatArgumentError(param, "No suitable resolver")));
}
private Mono<Object> resolveArg(HandlerMethodArgumentResolver resolver, MethodParameter parameter,
@@ -213,49 +220,60 @@ public class InvocableHandlerMethod extends HandlerMethod {
try {
return resolver.resolveArgument(parameter, bindingContext, exchange)
.defaultIfEmpty(NO_ARG_VALUE)
.doOnError(cause -> {
if (logger.isDebugEnabled()) {
logger.debug(getDetailedErrorMessage("Failed to resolve", parameter), cause);
}
});
.doOnError(cause -> logArgumentErrorIfNecessary(parameter, cause));
}
catch (Exception ex) {
throw getArgumentError("Failed to resolve", parameter, ex);
logArgumentErrorIfNecessary(parameter, ex);
return Mono.error(ex);
}
}
private IllegalStateException getArgumentError(String text, MethodParameter parameter, @Nullable Throwable ex) {
return new IllegalStateException(getDetailedErrorMessage(text, parameter), ex);
}
private String getDetailedErrorMessage(String text, MethodParameter param) {
return text + " argument " + param.getParameterIndex() + " of type '" +
param.getParameterType().getName() + "' on " + getBridgedMethod().toGenericString();
}
@Nullable
private Object doInvoke(Object[] args) throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("Invoking '" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) +
"' with arguments " + Arrays.toString(args));
private void logArgumentErrorIfNecessary(MethodParameter parameter, Throwable cause) {
// Leave stack trace for later, if error is not handled..
String message = cause.getMessage();
if (!message.contains(parameter.getExecutable().toGenericString())) {
if (logger.isDebugEnabled()) {
logger.debug(formatArgumentError(parameter, message));
}
}
ReflectionUtils.makeAccessible(getBridgedMethod());
Object returnValue = getBridgedMethod().invoke(getBean(), args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + ClassUtils.getQualifiedMethodName(getMethod(), getBeanType()) +
"] returned [" + returnValue + "]");
}
return returnValue;
}
private String getInvocationErrorMessage(Object[] args) {
String argumentDetails = IntStream.range(0, args.length)
private static String formatArgumentError(MethodParameter param, String message) {
return "Could not resolve parameter [" + param.getParameterIndex() + "] in " +
param.getExecutable().toGenericString() + (StringUtils.hasText(message) ? ": " + message : "");
}
/**
* Assert that the target bean class is an instance of the class where the given
* method is declared. In some cases the actual controller instance at request-
* processing time may be a JDK dynamic proxy (lazy initialization, prototype
* beans, and others). {@code @Controller}'s that require proxying should prefer
* class-based proxy mechanisms.
*/
private void assertTargetBean(Method method, Object targetBean, Object[] args) {
Class<?> methodDeclaringClass = method.getDeclaringClass();
Class<?> targetBeanClass = targetBean.getClass();
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
String text = "The mapped handler method class '" + methodDeclaringClass.getName() +
"' 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(formatInvokeError(text, args));
}
}
private String formatInvokeError(String text, Object[] args) {
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(",", " ", " "));
return "Failed to invoke handler method with resolved arguments:" + argumentDetails +
"on " + getBridgedMethod().toGenericString();
"[" + 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() + "]\n" +
"with argument values:\n" + formattedArgs;
}
private boolean isAsyncVoidReturnType(MethodParameter returnType,

View File

@@ -152,10 +152,18 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho
MediaType contentType = request.getHeaders().getContentType();
MediaType mediaType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
if (logger.isDebugEnabled()) {
logger.debug(contentType != null ? "Content-Type:" + contentType :
"No Content-Type, using " + MediaType.APPLICATION_OCTET_STREAM);
}
for (HttpMessageReader<?> reader : getMessageReaders()) {
if (reader.canRead(elementType, mediaType)) {
Map<String, Object> readHints = Collections.emptyMap();
if (adapter != null && adapter.isMultiValue()) {
if (logger.isDebugEnabled()) {
logger.debug("0..N [" + elementType + "]");
}
Flux<?> flux = reader.read(actualType, elementType, request, response, readHints);
flux = flux.onErrorResume(ex -> Flux.error(handleReadError(bodyParam, ex)));
if (isBodyRequired) {
@@ -170,6 +178,9 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho
}
else {
// Single-value (with or without reactive type wrapper)
if (logger.isDebugEnabled()) {
logger.debug("0..1 [" + elementType + "]");
}
Mono<?> mono = reader.readMono(actualType, elementType, request, response, readHints);
mono = mono.onErrorResume(ex -> Mono.error(handleReadError(bodyParam, ex)));
if (isBodyRequired) {

View File

@@ -141,6 +141,9 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa
ServerHttpResponse response = exchange.getResponse();
MediaType bestMediaType = selectMediaType(exchange, () -> getMediaTypesFor(elementType));
if (bestMediaType != null) {
if (logger.isDebugEnabled()) {
logger.debug((publisher instanceof Mono ? "0..1" : "0..N") + " [" + elementType + "]");
}
for (HttpMessageWriter<?> writer : getMessageWriters()) {
if (writer.canWrite(elementType, bestMediaType)) {
return writer.write((Publisher) publisher, actualType, elementType,

View File

@@ -205,10 +205,6 @@ class ControllerMethodResolver {
private void initControllerAdviceCaches(ApplicationContext applicationContext) {
if (logger.isInfoEnabled()) {
logger.info("Looking for @ControllerAdvice: " + applicationContext);
}
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(applicationContext);
AnnotationAwareOrderComparator.sort(beans);
@@ -218,26 +214,30 @@ class ControllerMethodResolver {
Set<Method> attrMethods = selectMethods(beanType, ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(bean, attrMethods);
if (logger.isInfoEnabled()) {
logger.info("Detected @ModelAttribute methods in " + bean);
}
}
Set<Method> binderMethods = selectMethods(beanType, BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(bean, binderMethods);
if (logger.isInfoEnabled()) {
logger.info("Detected @InitBinder methods in " + bean);
}
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(bean, resolver);
if (logger.isInfoEnabled()) {
logger.info("Detected @ExceptionHandler methods in " + bean);
}
}
}
}
if (logger.isDebugEnabled()) {
int modelSize = this.modelAttributeAdviceCache.size();
int binderSize = this.initBinderAdviceCache.size();
int handlerSize = this.exceptionHandlerAdviceCache.size();
if (modelSize == 0 && binderSize == 0 && handlerSize == 0) {
logger.debug("ControllerAdvice beans: none");
}
else {
logger.debug("ControllerAdvice beans: " + modelSize + " @ModelAttribute, " + binderSize +
" @InitBinder, " + handlerSize + " @ExceptionHandler");
}
}
}

View File

@@ -214,7 +214,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application
if (invocable != null) {
try {
if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod());
logger.debug("Using @ExceptionHandler " + invocable);
}
bindingContext.getModel().asMap().clear();
Throwable cause = exception.getCause();
@@ -227,7 +227,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application
}
catch (Throwable invocationEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to invoke: " + invocable.getMethod(), invocationEx);
logger.warn("Failure in @ExceptionHandler " + invocable, invocationEx);
}
}
}

View File

@@ -29,6 +29,7 @@ import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.ReactiveAdapter;
@@ -44,7 +45,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 5.0
*/
public abstract class AbstractView implements View, ApplicationContextAware {
public abstract class AbstractView implements View, BeanNameAware, ApplicationContextAware {
/** Well-known name for the RequestDataValueProcessor in the bean factory */
public static final String REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME = "requestDataValueProcessor";
@@ -65,6 +66,9 @@ public abstract class AbstractView implements View, ApplicationContextAware {
@Nullable
private String requestContextAttribute;
@Nullable
private String beanName;
@Nullable
private ApplicationContext applicationContext;
@@ -131,6 +135,24 @@ public abstract class AbstractView implements View, ApplicationContextAware {
return this.requestContextAttribute;
}
/**
* Set the view's name. Helpful for traceability.
* <p>Framework code must call this when constructing views.
*/
@Override
public void setBeanName(@Nullable String beanName) {
this.beanName = beanName;
}
/**
* Return the view's name. Should never be {@code null}, if the view was
* correctly configured.
*/
@Nullable
public String getBeanName() {
return this.beanName;
}
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
@@ -166,8 +188,9 @@ public abstract class AbstractView implements View, ApplicationContextAware {
public Mono<Void> render(@Nullable Map<String, ?> model, @Nullable MediaType contentType,
ServerWebExchange exchange) {
if (logger.isTraceEnabled()) {
logger.trace("Rendering view with model " + model);
if (logger.isDebugEnabled()) {
logger.debug("View " + formatViewName() +
", model " + (model != null ? model : Collections.emptyMap()));
}
if (contentType != null) {
@@ -298,7 +321,12 @@ public abstract class AbstractView implements View, ApplicationContextAware {
@Override
public String toString() {
return getClass().getName();
return getClass().getName() + ": " + formatViewName();
}
protected String formatViewName() {
return (getBeanName() != null ?
"name '" + getBeanName() + "'" : "[" + getClass().getSimpleName() + "]");
}
}

View File

@@ -23,6 +23,8 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@@ -43,6 +45,9 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class HttpMessageWriterView implements View {
private static final Log logger = LogFactory.getLog(HttpMessageWriter.class);
private final HttpMessageWriter<?> writer;
private final Set<String> modelKeys = new HashSet<>(4);
@@ -118,8 +123,7 @@ public class HttpMessageWriterView implements View {
Object value = getObjectToRender(model);
return (value != null) ?
write(value, contentType, exchange) :
exchange.getResponse().setComplete();
write(value, contentType, exchange) : exchange.getResponse().setComplete();
}
@Nullable

View File

@@ -167,9 +167,7 @@ public class FreeMarkerView extends AbstractUrlBasedView {
return true;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("No FreeMarker view found for URL: " + getUrl());
}
// Allow for ViewResolver chaining...
return false;
}
catch (ParseException ex) {
@@ -188,8 +186,9 @@ public class FreeMarkerView extends AbstractUrlBasedView {
// Expose all standard FreeMarker hash models.
SimpleHash freeMarkerModel = getTemplateModel(renderAttributes, exchange);
if (logger.isDebugEnabled()) {
logger.debug("Rendering FreeMarker template [" + getUrl() + "].");
logger.debug("Rendering [" + getUrl() + "]");
}
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());

View File

@@ -227,11 +227,11 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
protected void checkOnDataAvailable() {
resumeReceiving();
if (!this.pendingMessages.isEmpty()) {
logger.trace("checkOnDataAvailable, processing pending messages");
logger.trace("checkOnDataAvailable, " + this.pendingMessages.size() + " pending messages");
onDataAvailable();
}
else {
logger.trace("checkOnDataAvailable, no pending messages");
logger.trace("checkOnDataAvailable, 0 pending messages");
}
}
@@ -248,11 +248,11 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
void handleMessage(WebSocketMessage message) {
if (logger.isTraceEnabled()) {
logger.trace("Received message: " + message);
logger.trace("Received " + message);
}
if (!this.pendingMessages.offer(message)) {
throw new IllegalStateException("Too many messages received. " +
"Please ensure WebSocketSession.receive() is subscribed to.");
throw new IllegalStateException(
"Too many messages. Please ensure WebSocketSession.receive() is subscribed to.");
}
onDataAvailable();
}
@@ -266,7 +266,7 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
@Override
protected boolean write(WebSocketMessage message) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Sending message " + message);
logger.trace("Sending " + message);
}
return sendMessage(message);
}
@@ -288,7 +288,7 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
*/
public void setReadyToSend(boolean ready) {
if (ready) {
logger.trace("Send succeeded, ready to send again");
logger.trace("Ready to send again");
}
this.isReady = ready;
}

View File

@@ -43,14 +43,14 @@ public class WebSocketClientSupport {
protected List<String> beforeHandshake(URI url, HttpHeaders requestHeaders, WebSocketHandler handler) {
if (logger.isDebugEnabled()) {
logger.debug("Executing handshake to " + url);
logger.debug("Connecting to " + url);
}
return handler.getSubProtocols();
}
protected HandshakeInfo afterHandshake(URI url, HttpHeaders responseHeaders) {
if (logger.isDebugEnabled()) {
logger.debug("Handshake response: " + url + ", " + responseHeaders);
logger.debug("Connected to " + url + ", " + responseHeaders);
}
String protocol = responseHeaders.getFirst(SEC_WEBSOCKET_PROTOCOL);
return new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol);

View File

@@ -208,10 +208,6 @@ public class HandshakeWebSocketService implements WebSocketService, Lifecycle {
HttpMethod method = request.getMethod();
HttpHeaders headers = request.getHeaders();
if (logger.isDebugEnabled()) {
logger.debug("Handling " + request.getURI() + " with headers: " + headers);
}
if (HttpMethod.GET != method) {
return Mono.error(new MethodNotAllowedException(
request.getMethodValue(), Collections.singleton(HttpMethod.GET)));

View File

@@ -89,7 +89,7 @@ public class DispatcherHandlerErrorTests {
.consumeErrorWith(error -> {
assertThat(error, instanceOf(ResponseStatusException.class));
assertThat(error.getMessage(),
is("Response status 404 NOT_FOUND with reason \"No matching handler\""));
is("404 NOT_FOUND \"No matching handler\""));
})
.verify();
}

View File

@@ -41,10 +41,7 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -160,8 +157,8 @@ public class InvocableHandlerMethodTests {
fail("Expected IllegalStateException");
}
catch (IllegalStateException ex) {
assertThat(ex.getMessage(), is("No suitable resolver for argument 0 of type 'java.lang.String' " +
"on " + method.toGenericString()));
assertThat(ex.getMessage(), is("Could not resolve parameter [0] in " +
method.toGenericString() + ": No suitable resolver"));
}
}
@@ -176,12 +173,12 @@ public class InvocableHandlerMethodTests {
fail("Expected UnsupportedMediaTypeStatusException");
}
catch (UnsupportedMediaTypeStatusException ex) {
assertThat(ex.getMessage(), is("Response status 415 UNSUPPORTED_MEDIA_TYPE with reason \"boo\""));
assertThat(ex.getMessage(), is("415 UNSUPPORTED_MEDIA_TYPE \"boo\""));
}
}
@Test
public void illegalArgumentExceptionIsWrappedWithInvocationDetails() throws Exception {
public void illegalArgumentException() throws Exception {
Mono<Object> resolvedValue = Mono.just(1);
Method method = on(TestController.class).mockCall(o -> o.singleArg(null)).method();
Mono<HandlerResult> mono = invoke(new TestController(), method, resolverFor(resolvedValue));
@@ -191,9 +188,12 @@ public class InvocableHandlerMethodTests {
fail("Expected IllegalStateException");
}
catch (IllegalStateException ex) {
assertThat(ex.getMessage(), is("Failed to invoke handler method with resolved arguments: " +
"[0][type=java.lang.Integer][value=1] " +
"on " + method.toGenericString()));
assertNotNull("Exception not wrapped", ex.getCause());
assertTrue(ex.getCause() instanceof IllegalArgumentException);
assertTrue(ex.getMessage().contains("Controller ["));
assertTrue(ex.getMessage().contains("Method ["));
assertTrue(ex.getMessage().contains("with argument values:"));
assertTrue(ex.getMessage().contains("[0] [type=java.lang.Integer] [value=1]"));
}
}

View File

@@ -170,7 +170,7 @@ public class RequestMappingInfoHandlerMappingTests {
Mono<Object> mono = this.handlerMapping.getHandler(exchange);
assertError(mono, UnsupportedMediaTypeStatusException.class,
ex -> assertEquals("Response status 415 UNSUPPORTED_MEDIA_TYPE with reason " +
ex -> assertEquals("415 UNSUPPORTED_MEDIA_TYPE " +
"\"Invalid mime type \"bogus\": does not contain '/'\"", ex.getMessage()));
}

View File

@@ -6,8 +6,9 @@
</Console>
</Appenders>
<Loggers>
<Logger name="org.springframework.core.codec" level="debug" />
<Logger name="org.springframework.http" level="debug" />
<!--<Logger name="org.springframework.web" level="debug" />-->
<Logger name="org.springframework.web" level="debug" />
<!-- temporarily while we resolve random failures -->
<Logger name="org.springframework.web.reactive.socket.WebSocketIntegrationTests" level="debug" />