Improper Output Neutralization for Logs in Spring Framework
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
5.3.0 → fixed in 5.3.115.2.0 → fixed in 5.2.18
Details
In Spring Framework versions 5.3.0 - 5.3.10, 5.2.0 - 5.2.17, and older unsupported versions, it is possible for a user to provide malicious input to cause the insertion of additional log entries.
The fix
Release delta 5.3.0 → 5.3.11 (contains the fix)
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PrincipalMethodArgumentResolver.java+68 −0
@@ -0,0 +1,68 @@+/*+* Copyright 2002-2020 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+*+* https://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.web.servlet.mvc.method.annotation;++import java.security.Principal;++import javax.servlet.http.HttpServletRequest;++import org.springframework.core.MethodParameter;+import org.springframework.lang.Nullable;+import org.springframework.web.bind.support.WebDataBinderFactory;+import org.springframework.web.context.request.NativeWebRequest;+import org.springframework.web.method.support.HandlerMethodArgumentResolver;+import org.springframework.web.method.support.ModelAndViewContainer;++/**+* Resolves an argument of type {@link Principal}, similar to+* {@link ServletRequestMethodArgumentResolver} but irrespective of whether the+* argument is annotated or not. This is doen to enable custom argument+* resolution of a {@link Principal} argument (with custom annotation).+*+* @author Rossen Stoyanchev+* @since 5.3.1+*/+public class PrincipalMethodArgumentResolver implements HandlerMethodArgumentResolver {+++@Override+public boolean supportsParameter(MethodParameter parameter) {+Class<?> paramType = parameter.getParameterType();+return Principal.class.isAssignableFrom(paramType);+}++@Override+public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,+NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {++Class<?> paramType = parameter.getParameterType();++HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);+if (request == null) {+throw new IllegalStateException(+"Current request is not of type [HttpServletRequest]: " + webRequest);+}++Principal principal = request.getUserPrincipal();+if (principal != null && !paramType.isInstance(principal)) {+throw new IllegalStateException(+"Current user principal is not of type [" + paramType.getName() + "]: " + principal);+}++return principal;+}++}
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/PrincipalMethodArgumentResolverTests.java+110 −0
@@ -0,0 +1,110 @@+/*+* Copyright 2002-2020 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+*+* https://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.web.servlet.mvc.method.annotation;++import java.lang.annotation.ElementType;+import java.lang.annotation.Retention;+import java.lang.annotation.RetentionPolicy;+import java.lang.annotation.Target;+import java.lang.reflect.Method;+import java.security.Principal;++import javax.servlet.ServletRequest;++import org.junit.jupiter.api.BeforeEach;+import org.junit.jupiter.api.Test;++import org.springframework.core.MethodParameter;+import org.springframework.web.context.request.ServletWebRequest;+import org.springframework.web.method.support.ModelAndViewContainer;+import org.springframework.web.testfixture.servlet.MockHttpServletRequest;+import org.springframework.web.testfixture.servlet.MockHttpServletResponse;++import static org.assertj.core.api.Assertions.assertThat;++/**+* Unit tests for {@link PrincipalMethodArgumentResolver}.+*+* @author Rossen Stoyanchev+*/+public class PrincipalMethodArgumentResolverTests {++private PrincipalMethodArgumentResolver resolver;++private ModelAndViewContainer mavContainer;++private MockHttpServletRequest servletRequest;++private ServletWebRequest webRequest;++private Method method;+++@BeforeEach+public void setup() throws Exception {+resolver = new PrincipalMethodArgumentResolver();+mavContainer = new ModelAndViewContainer();+servletRequest = new MockHttpServletRequest("GET", "");+webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse());++method = getClass().getMethod("supportedParams", ServletRequest.class, Principal.class);+}+++@Test+public void principal() throws Exception {+Principal principal = () -> "Foo";+servletRequest.setUserPrincipal(principal);++MethodParameter principalParameter = new MethodParameter(method, 1);+assertThat(resolver.supportsParameter(principalParameter)).as("Principal not supported").isTrue();++Object result = resolver.resolveArgument(principalParameter, null, webRequest, null);+assertThat(result).as("Invalid result").isSameAs(principal);+}++@Test+public void principalAsNull() throws Exception {+MethodParameter principalParameter = new MethodParameter(method, 1);+assertThat(resolver.supportsParameter(principalParameter)).as("Principal not supported").isTrue();++Object result = resolver.resolveArgument(principalParameter, null, webRequest, null);+assertThat(result).as("Invalid result").isNull();+}++@Test // gh-25780+public void annotatedPrincipal() throws Exception {+Principal principal = () -> "Foo";+servletRequest.setUserPrincipal(principal);+Method principalMethod = getClass().getMethod("supportedParamsWithAnnotatedPrincipal", Principal.class);++MethodParameter principalParameter = new MethodParameter(principalMethod, 0);+assertThat(resolver.supportsParameter(principalParameter)).isTrue();+}+++@SuppressWarnings("unused")+public void supportedParams(ServletRequest p0, Principal p1) {}++@Target({ ElementType.PARAMETER })+@Retention(RetentionPolicy.RUNTIME)+public @interface AuthenticationPrincipal {}++@SuppressWarnings("unused")+public void supportedParamsWithAnnotatedPrincipal(@AuthenticationPrincipal Principal p) {}++}core-resources.adocsrc/docs/asciidoc/core/core-resources.adoc | 2 +-1 file changed, 1 insertion(+), 1 deletion(-)
spring-tx/src/test/kotlin/org/springframework/transaction/annotation/CoroutinesAnnotationTransactionInterceptorTests.kt+117 −0
@@ -0,0 +1,117 @@+package org.springframework.transaction.annotation++import kotlinx.coroutines.delay+import kotlinx.coroutines.runBlocking+import org.assertj.core.api.Assertions+import org.junit.jupiter.api.Disabled+import org.junit.jupiter.api.Test+import org.springframework.aop.framework.ProxyFactory+import org.springframework.transaction.TransactionManager+import org.springframework.transaction.interceptor.TransactionInterceptor+import org.springframework.transaction.testfixture.CallCountingTransactionManager+import org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager++class CoroutinesAnnotationTransactionInterceptorTests {++private val ptm = CallCountingTransactionManager()++private val rtm = ReactiveCallCountingTransactionManager()++private val source = AnnotationTransactionAttributeSource()++private val ti = TransactionInterceptor((ptm as TransactionManager), source)++@Test+fun suspendingNoValueSuccess() {+val proxyFactory = ProxyFactory()+proxyFactory.setTarget(TestWithCoroutines())+proxyFactory.addAdvice(TransactionInterceptor(rtm, source))+val proxy = proxyFactory.proxy as TestWithCoroutines+runBlocking {+proxy.suspendingNoValueSuccess()+}+assertReactiveGetTransactionAndCommitCount(1)+}++@Test+fun suspendingNoValueFailure() {+val proxyFactory = ProxyFactory()+proxyFactory.setTarget(TestWithCoroutines())+proxyFactory.addAdvice(TransactionInterceptor(rtm, source))+val proxy = proxyFactory.proxy as TestWithCoroutines+runBlocking {+try {+proxy.suspendingNoValueFailure()+}+catch (ex: IllegalStateException) {+}++}+assertReactiveGetTransactionAndRollbackCount(1)+}++@Test+@Disabled("Currently fails due to gh-25998")+fun suspendingValueSuccess() {+val proxyFactory = ProxyFactory()+proxyFactory.setTarget(TestWithCoroutines())+proxyFactory.addAdvice(TransactionInterceptor(rtm, source))+val proxy = proxyFactory.proxy as TestWithCoroutines+runBlocking {+Assertions.assertThat(proxy.suspendingValueSuccess()).isEqualTo("foo")+}+assertReactiveGetTransactionAndCommitCount(1)+}++@Test+fun suspendingValueFailure() {+val proxyFactory = ProxyFactory()+proxyFactory.setTarget(TestWithCoroutines())+proxyFactory.addAdvice(TransactionInterceptor(rtm, source))+val proxy = proxyFactory.proxy as TestWithCoroutines+runBlocking {+try {+proxy.suspendingValueFailure()+}+catch (ex: IllegalStateException) {+}++}+assertReactiveGetTransactionAndRollbackCount(1)+}++++private fun assertReactiveGetTransactionAndCommitCount(expectedCount: Int) {+Assertions.assertThat(rtm.begun).isEqualTo(expectedCount)+Assertions.assertThat(rtm.commits).isEqualTo(expectedCount)+}++private fun assertReactiveGetTransactionAndRollbackCount(expectedCount: Int) {+Assertions.assertThat(rtm.begun).isEqualTo(expectedCount)+Assertions.assertThat(rtm.rollbacks).isEqualTo(expectedCount)+}++@Transactional+open class TestWithCoroutines {++open suspend fun suspendingNoValueSuccess() {+delay(10)+}++open suspend fun suspendingNoValueFailure() {+delay(10)+throw IllegalStateException()+}++open suspend fun suspendingValueSuccess(): String {+delay(10)+return "foo"+}++open suspend fun suspendingValueFailure(): String {+delay(10)+throw IllegalStateException()+}+}+}.../CoroutinesAnnotationTransactionInterceptorTests.kt | 2 --1 file changed, 2 deletions(-)
spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java+50 −28
@@ -1,5 +1,5 @@/*-* Copyright 2002-2018 the original author or authors.+* Copyright 2002-2020 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.@@ -63,27 +63,29 @@* <p>Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}.** @author Andy Clement+* @author Juergen Hoeller* @since 4.1*/public final class SpelCompiler implements Opcodes {-private static final Log logger = LogFactory.getLog(SpelCompiler.class);+private static final int CLASSES_DEFINED_LIMIT = 10;-private static final int CLASSES_DEFINED_LIMIT = 100;+private static final Log logger = LogFactory.getLog(SpelCompiler.class);// A compiler is created for each classloader, it manages a child class loader of that// classloader and the child is used to load the compiled expressions.private static final Map<ClassLoader, SpelCompiler> compilers = new ConcurrentReferenceHashMap<>();+// The child ClassLoader used to load the compiled expression classes-private ChildClassLoader ccl;+private volatile ChildClassLoader childClassLoader;// Counter suffix for generated classes within this SpelCompiler instanceprivate final AtomicInteger suffixId = new AtomicInteger(1);private SpelCompiler(@Nullable ClassLoader classloader) {-this.ccl = new ChildClassLoader(classloader);+this.childClassLoader = new ChildClassLoader(classloader);}@@ -135,7 +137,7 @@ private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl e// Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression'String className = "spel/Ex" + getNextSuffix();ClassWriter cw = new ExpressionClassWriter();-cw.visit(V1_5, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);+cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);// Create default constructorMethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);@@ -150,7 +152,7 @@ private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl e// Create getValue() methodmv = cw.visitMethod(ACC_PUBLIC, "getValue","(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null,-new String[ ]{"org/springframework/expression/EvaluationException"});+new String[] {"org/springframework/expression/EvaluationException"});mv.visitCode();CodeFlow cf = new CodeFlow(className, cw);@@ -187,7 +189,7 @@ private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl e/*** Load a compiled expression class. Makes sure the classloaders aren't used too much-* because they anchor compiled classes in memory and prevent GC. If you have expressions+* because they anchor compiled classes in memory and prevent GC. If you have expressions* continually recompiling over time then by replacing the classloader periodically* at least some of the older variants can be garbage collected.* @param name the name of the class@@ -196,12 +198,25 @@ private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl e*/@SuppressWarnings("unchecked")private Class<? extends CompiledExpression> loadClass(String name, byte[] bytes) {-if (this.ccl.getClassesDefinedCount() > CLASSES_DEFINED_LIMIT) {-this.ccl = new ChildClassLoader(this.ccl.getParent());+ChildClassLoader ccl = this.childClassLoader;+if (ccl.getClassesDefinedCount() >= CLASSES_DEFINED_LIMIT) {+synchronized (this) {+ChildClassLoader currentCcl = this.childClassLoader;+if (ccl == currentCcl) {+// Still the same ClassLoader that needs to be replaced...+ccl = new ChildClassLoader(ccl.getParent());+this.childClassLoader = ccl;+}+else {+// Already replaced by some other thread, let's pick it up.+ccl = currentCcl;+}+}}-return (Class<? extends CompiledExpression>) this.ccl.defineClass(name, bytes);+return (Class<? extends CompiledExpression>) ccl.defineClass(name, bytes);}+/*** Factory method for compiler instances. The returned SpelCompiler will* attach a class loader as the child of the given class loader and this@@ -211,21 +226,28 @@ private Class<? extends CompiledExpression> loadClass(String name, byte[] bytes)*/public static SpelCompiler getCompiler(@Nullable ClassLoader classLoader) {ClassLoader clToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());-synchronized (compilers) {-SpelCompiler compiler = compilers.get(clToUse);-if (compiler == null) {-compiler = new SpelCompiler(clToUse);-compilers.put(clToUse, compiler);+// Quick check for existing compiler without lock contention+SpelCompiler compiler = compilers.get(clToUse);+if (compiler == null) {+// Full lock now since we're creating a child ClassLoader+synchronized (compilers) {+compiler = compilers.get(clToUse);+if (compiler == null) {+compiler = new SpelCompiler(clToUse);+compilers.put(clToUse, compiler);+}}-return compiler;}+return compiler;}/**-* Request that an attempt is made to compile the specified expression. It may fail if-* components of the expression are not suitable for compilation or the data types-* involved are not suitable for compilation. Used for testing.-* @return true if the expression was successfully compiled+* Request that an attempt is made to compile the specified expression.+* It may fail if components of the expression are not suitable for compilation+* or the data types involved are not suitable for compilation. Used for testing.+* @param expression the expression to compile+* @return {@code true} if the expression was successfully compiled,+* {@code false} otherwise*/public static boolean compile(Expression expression) {return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression());@@ -250,21 +272,21 @@ private static class ChildClassLoader extends URLClassLoader {private static final URL[] NO_URLS = new URL[0];-private int classesDefinedCount = 0;+private final AtomicInteger classesDefinedCount = new AtomicInteger(0);public ChildClassLoader(@Nullable ClassLoader classLoader) {super(NO_URLS, classLoader);}-int getClassesDefinedCount() {-return this.classesDefinedCount;-}-public Class<?> defineClass(String name, byte[] bytes) {Class<?> clazz = super.defineClass(name, bytes, 0, bytes.length);-this.classesDefinedCount++;+this.classesDefinedCount.incrementAndGet();return clazz;}++public int getClassesDefinedCount() {+return this.classesDefinedCount.get();+}}@@ -276,7 +298,7 @@ public ExpressionClassWriter() {@Overrideprotected ClassLoader getClassLoader() {-return ccl;+return childClassLoader;}}.../expression/spel/standard/SpelCompiler.java | 9 ++++++---.../web/util/UriComponentsBuilderTests.java | 6 +++---2 files changed, 9 insertions(+), 6 deletions(-)
spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java+24 −2
@@ -50,6 +50,7 @@import org.springframework.cache.CacheManager;import org.springframework.cache.concurrent.ConcurrentMapCache;import org.springframework.context.i18n.LocaleContextHolder;+import org.springframework.context.support.StaticApplicationContext;import org.springframework.core.Ordered;import org.springframework.core.convert.ConversionService;import org.springframework.core.io.Resource;@@ -100,6 +101,7 @@import org.springframework.web.servlet.handler.MappedInterceptor;import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor;+import org.springframework.web.servlet.i18n.CookieLocaleResolver;import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;import org.springframework.web.servlet.mvc.ParameterizableViewController;@@ -122,9 +124,12 @@import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;import org.springframework.web.servlet.resource.VersionResourceResolver;import org.springframework.web.servlet.resource.WebJarsResourceResolver;+import org.springframework.web.servlet.support.SessionFlashMapManager;+import org.springframework.web.servlet.theme.CookieThemeResolver;import org.springframework.web.servlet.theme.ThemeChangeInterceptor;import org.springframework.web.servlet.view.BeanNameViewResolver;import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;+import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator;import org.springframework.web.servlet.view.InternalResourceView;import org.springframework.web.servlet.view.InternalResourceViewResolver;import org.springframework.web.servlet.view.RedirectView;@@ -225,10 +230,10 @@ public void testDefaultConfig() throws Exception {assertThat(appContext.getBean(ConversionService.class)).isNotNull();assertThat(appContext.getBean(LocalValidatorFactoryBean.class)).isNotNull();assertThat(appContext.getBean(Validator.class)).isNotNull();-assertThat(appContext.getBean("themeResolver", ThemeResolver.class)).isNotNull();assertThat(appContext.getBean("localeResolver", LocaleResolver.class)).isNotNull();-assertThat(appContext.getBean("flashMapManager", FlashMapManager.class)).isNotNull();+assertThat(appContext.getBean("themeResolver", ThemeResolver.class)).isNotNull();assertThat(appContext.getBean("viewNameTranslator", RequestToViewNameTranslator.class)).isNotNull();+assertThat(appContext.getBean("flashMapManager", FlashMapManager.class)).isNotNull();// default web binding initializer behavior testrequest = new MockHttpServletRequest("GET", "/");@@ -262,6 +267,23 @@ public void testDefaultConfig() throws Exception {assertThat(introspector.getHandlerMappings().get(1).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class);}+@Test // gh-25290+public void testDefaultConfigWithBeansInParentContext() throws Exception {+StaticApplicationContext parent = new StaticApplicationContext();+parent.registerSingleton("localeResolver", CookieLocaleResolver.class);+parent.registerSingleton("themeResolver", CookieThemeResolver.class);+parent.registerSingleton("viewNameTranslator", DefaultRequestToViewNameTranslator.class);+parent.registerSingleton("flashMapManager", SessionFlashMapManager.class);+parent.refresh();+appContext.setParent(parent);++loadBeanDefinitions("mvc-config.xml");+assertThat(appContext.getBean("localeResolver")).isSameAs(parent.getBean("localeResolver"));+assertThat(appContext.getBean("themeResolver")).isSameAs(parent.getBean("themeResolver"));+assertThat(appContext.getBean("viewNameTranslator")).isSameAs(parent.getBean("viewNameTranslator"));+assertThat(appContext.getBean("flashMapManager")).isSameAs(parent.getBean("flashMapManager"));+}+@Testpublic void testCustomConversionService() throws Exception {loadBeanDefinitions("mvc-config-custom-conversion-service.xml");UrlPathHelper.removeSemicolonContentInternal().../main/java/org/springframework/web/util/UrlPathHelper.java | 4 ++--.../java/org/springframework/web/util/UrlPathHelperTests.java | 3 +++2 files changed, 5 insertions(+), 2 deletions(-)
spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java+4 −1
@@ -18,6 +18,7 @@import java.io.IOException;import java.io.InputStreamReader;+import java.io.OutputStream;import java.io.Reader;import java.lang.reflect.Type;import java.nio.charset.Charset;@@ -55,6 +56,7 @@import org.springframework.lang.Nullable;import org.springframework.util.Assert;import org.springframework.util.CollectionUtils;+import org.springframework.util.StreamUtils;import org.springframework.util.TypeUtils;/**@@ -308,7 +310,8 @@ protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessaMediaType contentType = outputMessage.getHeaders().getContentType();JsonEncoding encoding = getJsonEncoding(contentType);-try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding)) {+OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());+try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding)) {writePrefix(generator, object);Object value = object;
spring-webflux/src/test/java/org/springframework/web/reactive/function/server/CustomRouteBuilder.java+0 −157
@@ -1,157 +0,0 @@-/*-* Copyright 2002-2020 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-*-* https://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.web.reactive.function.server;--import java.util.LinkedHashMap;-import java.util.Map;-import java.util.function.Consumer;-import java.util.function.Function;--import reactor.core.publisher.Mono;--import org.springframework.core.io.Resource;-import org.springframework.lang.Nullable;--/**-* @author Arjen Poutsma-*/-public class CustomRouteBuilder {--public static final String OPERATION_ATTRIBUTE = CustomRouteBuilder.class.getName() + ".operation";--private final RouterFunctions.Builder delegate = RouterFunctions.route();---private CustomRouteBuilder() {-}--public static CustomRouteBuilder route() {-return new CustomRouteBuilder();-}--public CustomRouteBuilder GET(String pattern, HandlerFunction<ServerResponse> handlerFunction,-Consumer<OperationBuilder> operationsConsumer) {--OperationBuilder builder = new OperationBuilder();-operationsConsumer.accept(builder);--this.delegate.GET(pattern, handlerFunction)-.withAttribute(OPERATION_ATTRIBUTE, builder.operation);--return this;-}--public RouterFunction<ServerResponse> build() {-return this.delegate.build();-}--public static void main(String[] args) {-RouterFunction<ServerResponse> routerFunction =-route()-.GET("/foo", request -> ServerResponse.ok().build(), ops -> ops-.parameter("key1", "My key1 description")-.parameter("key1", "My key1 description")-.response(200, "This is normal response description")-.response(404, "This is response description")-)-.build();--AttributesVisitor visitor = new AttributesVisitor();-routerFunction.accept(visitor);-}---public static class OperationBuilder {--private final Operation operation = new Operation();--public OperationBuilder parameter(String name, String description) {-this.operation.parameter(name, description);-return this;-}--public OperationBuilder response(int statusCode, String description) {-this.operation.response(statusCode, description);-return this;-}--}---static class Operation {--private final Map<String, String> parameters = new LinkedHashMap<>();--private final Map<Integer, String > responses = new LinkedHashMap<>();--public void parameter(String name, String description) {-this.parameters.put(name, description);-}--public void response(int status, String description) {-this.responses.put(status, description);-}--@Override-public String toString() {-return "parameters=" + parameters +-", responses=" + responses;-}-}--static class AttributesVisitor implements RouterFunctions.Visitor {--@Nullable-private Map<String, Object> attributes;--@Override-public void attributes(Map<String, Object> attributes) {-this.attributes = attributes;-}--@Override-public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {-System.out.printf("Route predicate %s->%s%nhas attributes %s", predicate, handlerFunction, this.attributes);-this.attributes = null;-}--@Override-public void startNested(RequestPredicate predicate) {-// TODO-}--@Override-public void endNested(RequestPredicate predicate) {-// TODO--}--@Override-public void resources(Function<ServerRequest, Mono<Resource>> lookupFunction) {-// TODO--}--@Override-public void unknown(RouterFunction<?> routerFunction) {-// TODO--}-}---}.../client/DefaultWebClientBuilder.java | 32 +++++++++++++++++--.../client/DefaultWebClientTests.java | 32 ++++++++++++++++++-2 files changed, 61 insertions(+), 3 deletions(-)
spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java+15 −2
@@ -21,6 +21,7 @@import java.math.BigDecimal;import java.util.List;import java.util.Map;+import java.util.concurrent.atomic.AtomicReference;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.DeserializationFeature;@@ -99,8 +100,20 @@ public int getMaxInMemorySize() {public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {JavaType javaType = getObjectMapper().constructType(elementType.getType());// Skip String: CharSequenceDecoder + "*/*" comes after-return (!CharSequence.class.isAssignableFrom(elementType.toClass()) &&-getObjectMapper().canDeserialize(javaType) && supportsMimeType(mimeType));+if (CharSequence.class.isAssignableFrom(elementType.toClass()) || !supportsMimeType(mimeType)) {+return false;+}+if (!logger.isDebugEnabled()) {+return getObjectMapper().canDeserialize(javaType);+}+else {+AtomicReference<Throwable> causeRef = new AtomicReference<>();+if (getObjectMapper().canDeserialize(javaType, causeRef)) {+return true;+}+logWarningIfNecessary(javaType, causeRef.get());+return false;+}}@Override
spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java+18 −2
@@ -23,6 +23,7 @@import java.util.Collections;import java.util.List;import java.util.Map;+import java.util.concurrent.atomic.AtomicReference;import com.fasterxml.jackson.core.JsonEncoding;import com.fasterxml.jackson.core.JsonGenerator;@@ -111,8 +112,23 @@ public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeTypereturn false;}}-return (Object.class == clazz ||-(!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));+if (String.class.isAssignableFrom(elementType.resolve(clazz))) {+return false;+}+if (Object.class == clazz) {+return true;+}+if (!logger.isDebugEnabled()) {+return getObjectMapper().canSerialize(clazz);+}+else {+AtomicReference<Throwable> causeRef = new AtomicReference<>();+if (getObjectMapper().canSerialize(clazz, causeRef)) {+return true;+}+logWarningIfNecessary(clazz, causeRef.get());+return false;+}}@Override
spring-websocket/src/main/java/org/springframework/web/socket/messaging/DefaultSimpUserRegistry.java+6 −9
@@ -27,6 +27,7 @@import org.springframework.core.Ordered;import org.springframework.lang.Nullable;import org.springframework.messaging.Message;+import org.springframework.messaging.MessageHeaders;import org.springframework.messaging.simp.SimpMessageHeaderAccessor;import org.springframework.messaging.simp.user.DestinationUserNameProvider;import org.springframework.messaging.simp.user.SimpSession;@@ -34,7 +35,6 @@import org.springframework.messaging.simp.user.SimpSubscriptionMatcher;import org.springframework.messaging.simp.user.SimpUser;import org.springframework.messaging.simp.user.SimpUserRegistry;-import org.springframework.messaging.support.MessageHeaderAccessor;import org.springframework.util.Assert;/**@@ -84,19 +84,16 @@ public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {public void onApplicationEvent(ApplicationEvent event) {AbstractSubProtocolEvent subProtocolEvent = (AbstractSubProtocolEvent) event;Message<?> message = subProtocolEvent.getMessage();+MessageHeaders headers = message.getHeaders();-SimpMessageHeaderAccessor accessor =-MessageHeaderAccessor.getAccessor(message, SimpMessageHeaderAccessor.class);-Assert.state(accessor != null, "No SimpMessageHeaderAccessor");--String sessionId = accessor.getSessionId();+String sessionId = SimpMessageHeaderAccessor.getSessionId(headers);Assert.state(sessionId != null, "No session id");if (event instanceof SessionSubscribeEvent) {LocalSimpSession session = this.sessions.get(sessionId);if (session != null) {-String id = accessor.getSubscriptionId();-String destination = accessor.getDestination();+String id = SimpMessageHeaderAccessor.getSubscriptionId(headers);+String destination = SimpMessageHeaderAccessor.getDestination(headers);if (id != null && destination != null) {session.addSubscription(id, destination);}@@ -137,7 +134,7 @@ else if (event instanceof SessionDisconnectEvent) {else if (event instanceof SessionUnsubscribeEvent) {LocalSimpSession session = this.sessions.get(sessionId);if (session != null) {-String subscriptionId = accessor.getSubscriptionId();+String subscriptionId = SimpMessageHeaderAccessor.getSubscriptionId(headers);if (subscriptionId != null) {session.removeSubscription(subscriptionId);}.../codec/json/AbstractJackson2Decoder.java | 17 +++++++++--.../codec/json/AbstractJackson2Encoder.java | 20 +++++++++++--.../http/codec/json/Jackson2CodecSupport.java | 30 ++++++++++++++++++-3 files changed, 62 insertions(+), 5 deletions(-)
spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java+29 −1
@@ -26,6 +26,7 @@import com.fasterxml.jackson.annotation.JsonView;import com.fasterxml.jackson.databind.JavaType;+import com.fasterxml.jackson.databind.JsonMappingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.apache.commons.logging.Log;@@ -108,7 +109,34 @@ protected List<MimeType> getMimeTypes() {protected boolean supportsMimeType(@Nullable MimeType mimeType) {-return (mimeType == null || this.mimeTypes.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));+if (mimeType == null) {+return true;+}+for (MimeType supportedMimeType : this.mimeTypes) {+if (supportedMimeType.isCompatibleWith(mimeType)) {+return true;+}+}+return false;+}++/**+* Determine whether to log the given exception coming from a+* {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check.+* @param type the class that Jackson tested for (de-)serializability+* @param cause the Jackson-thrown exception to evaluate+* (typically a {@link JsonMappingException})+* @since 5.3.1+*/+protected void logWarningIfNecessary(Type type, @Nullable Throwable cause) {+if (cause == null) {+return;+}+if (logger.isDebugEnabled()) {+String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") ++"serialization for type [" + type + "]";+logger.debug(msg, cause);+}}protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {.../test/context/support/ActiveProfilesUtils.java | 9 ++++-----1 file changed, 4 insertions(+), 5 deletions(-)
spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java+19 −4
@@ -34,6 +34,7 @@import com.fasterxml.jackson.databind.ObjectWriter;import com.fasterxml.jackson.databind.SequenceWriter;import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;+import com.fasterxml.jackson.databind.ser.FilterProvider;import org.reactivestreams.Publisher;import reactor.core.publisher.Flux;import reactor.core.publisher.Mono;@@ -48,6 +49,7 @@import org.springframework.core.log.LogFormatUtils;import org.springframework.http.MediaType;import org.springframework.http.codec.HttpMessageEncoder;+import org.springframework.http.converter.json.MappingJacksonValue;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.lang.Nullable;@@ -148,7 +150,7 @@ public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory buffebyte[] separator = getStreamingMediaTypeSeparator(mimeType);if (separator != null) { // streamingtry {-ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);+ObjectWriter writer = createObjectWriter(elementType, mimeType, null, hints);ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());JsonEncoding encoding = getJsonEncoding(mimeType);JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding);@@ -186,7 +188,18 @@ public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory buffepublic DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {-ObjectWriter writer = createObjectWriter(valueType, mimeType, hints);+Class<?> jsonView = null;+FilterProvider filters = null;+if (value instanceof MappingJacksonValue) {+MappingJacksonValue container = (MappingJacksonValue) value;+value = container.getValue();+jsonView = container.getSerializationView();+filters = container.getFilters();+}+ObjectWriter writer = createObjectWriter(valueType, mimeType, jsonView, hints);+if (filters != null) {+writer = writer.with(filters);+}ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());try {JsonEncoding encoding = getJsonEncoding(mimeType);@@ -268,10 +281,12 @@ private void logValue(@Nullable Map<String, Object> hints, Object value) {}private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType,-@Nullable Map<String, Object> hints) {+@Nullable Class<?> jsonView, @Nullable Map<String, Object> hints) {JavaType javaType = getJavaType(valueType.getType(), null);-Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);+if (jsonView == null && hints != null) {+jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);+}ObjectWriter writer = (jsonView != null ?getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer());
spring-tx/src/test/kotlin/org/springframework/transaction/annotation/CoroutinesAnnotationTransactionInterceptorTests.kt+0 −6
@@ -22,9 +22,7 @@ import org.assertj.core.api.Assertionsimport org.junit.jupiter.api.Disabledimport org.junit.jupiter.api.Testimport org.springframework.aop.framework.ProxyFactory-import org.springframework.transaction.TransactionManagerimport org.springframework.transaction.interceptor.TransactionInterceptor-import org.springframework.transaction.testfixture.CallCountingTransactionManagerimport org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager/**@@ -32,14 +30,10 @@ import org.springframework.transaction.testfixture.ReactiveCallCountingTransacti*/class CoroutinesAnnotationTransactionInterceptorTests {-private val ptm = CallCountingTransactionManager()-private val rtm = ReactiveCallCountingTransactionManager()private val source = AnnotationTransactionAttributeSource()-private val ti = TransactionInterceptor((ptm as TransactionManager), source)-@Testfun suspendingNoValueSuccess() {val proxyFactory = ProxyFactory()parent context as well.../web/servlet/config/MvcNamespaceUtils.java | 123 ++++++++++--------.../web/servlet/config/MvcNamespaceTests.java | 26 +++-2 files changed, 94 insertions(+), 55 deletions(-)
spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java+70 −53
@@ -19,9 +19,11 @@import java.util.LinkedHashMap;import java.util.Map;+import org.springframework.beans.factory.BeanFactory;import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.config.RuntimeBeanReference;import org.springframework.beans.factory.parsing.BeanComponentDefinition;+import org.springframework.beans.factory.support.BeanDefinitionRegistry;import org.springframework.beans.factory.support.RootBeanDefinition;import org.springframework.beans.factory.xml.ParserContext;import org.springframework.lang.Nullable;@@ -43,6 +45,7 @@* Convenience methods for use in MVC namespace BeanDefinitionParsers.** @author Rossen Stoyanchev+* @author Juergen Hoeller* @author Brian Clozel* @author Marten Deinum* @since 3.1@@ -67,15 +70,15 @@ public abstract class MvcNamespaceUtils {private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";-public static void registerDefaultComponents(ParserContext parserContext, @Nullable Object source) {-registerBeanNameUrlHandlerMapping(parserContext, source);-registerHttpRequestHandlerAdapter(parserContext, source);-registerSimpleControllerHandlerAdapter(parserContext, source);-registerHandlerMappingIntrospector(parserContext, source);-registerThemeResolver(parserContext, source);-registerLocaleResolver(parserContext, source);-registerFlashMapManager(parserContext, source);-registerViewNameTranslator(parserContext, source);+public static void registerDefaultComponents(ParserContext context, @Nullable Object source) {+registerBeanNameUrlHandlerMapping(context, source);+registerHttpRequestHandlerAdapter(context, source);+registerSimpleControllerHandlerAdapter(context, source);+registerHandlerMappingIntrospector(context, source);+registerLocaleResolver(context, source);+registerThemeResolver(context, source);+registerViewNameTranslator(context, source);+registerFlashMapManager(context, source);}/**@@ -84,21 +87,21 @@ public static void registerDefaultComponents(ParserContext parserContext, @Nulla* @return a RuntimeBeanReference to this {@link UrlPathHelper} instance*/public static RuntimeBeanReference registerUrlPathHelper(-@Nullable RuntimeBeanReference urlPathHelperRef, ParserContext parserContext, @Nullable Object source) {+@Nullable RuntimeBeanReference urlPathHelperRef, ParserContext context, @Nullable Object source) {if (urlPathHelperRef != null) {-if (parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME)) {-parserContext.getRegistry().removeAlias(URL_PATH_HELPER_BEAN_NAME);+if (context.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME)) {+context.getRegistry().removeAlias(URL_PATH_HELPER_BEAN_NAME);}-parserContext.getRegistry().registerAlias(urlPathHelperRef.getBeanName(), URL_PATH_HELPER_BEAN_NAME);+context.getRegistry().registerAlias(urlPathHelperRef.getBeanName(), URL_PATH_HELPER_BEAN_NAME);}-else if (!parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) &&-!parserContext.getRegistry().containsBeanDefinition(URL_PATH_HELPER_BEAN_NAME)) {+else if (!context.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) &&+!context.getRegistry().containsBeanDefinition(URL_PATH_HELPER_BEAN_NAME)) {RootBeanDefinition urlPathHelperDef = new RootBeanDefinition(UrlPathHelper.class);urlPathHelperDef.setSource(source);urlPathHelperDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);-parserContext.getRegistry().registerBeanDefinition(URL_PATH_HELPER_BEAN_NAME, urlPathHelperDef);-parserContext.registerComponent(new BeanComponentDefinition(urlPathHelperDef, URL_PATH_HELPER_BEAN_NAME));+context.getRegistry().registerBeanDefinition(URL_PATH_HELPER_BEAN_NAME, urlPathHelperDef);+context.registerComponent(new BeanComponentDefinition(urlPathHelperDef, URL_PATH_HELPER_BEAN_NAME));}return new RuntimeBeanReference(URL_PATH_HELPER_BEAN_NAME);}@@ -109,21 +112,21 @@ else if (!parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) &&* @return a RuntimeBeanReference to this {@link PathMatcher} instance*/public static RuntimeBeanReference registerPathMatcher(@Nullable RuntimeBeanReference pathMatcherRef,-ParserContext parserContext, @Nullable Object source) {+ParserContext context, @Nullable Object source) {if (pathMatcherRef != null) {-if (parserContext.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {-parserContext.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME);+if (context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {+context.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME);}-parserContext.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME);+context.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME);}-else if (!parserContext.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&-!parserContext.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {+else if (!context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&+!context.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {RootBeanDefinition pathMatcherDef = new RootBeanDefinition(AntPathMatcher.class);pathMatcherDef.setSource(source);pathMatcherDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);-parserContext.getRegistry().registerBeanDefinition(PATH_MATCHER_BEAN_NAME, pathMatcherDef);-parserContext.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATH_MATCHER_BEAN_NAME));+context.getRegistry().registerBeanDefinition(PATH_MATCHER_BEAN_NAME, pathMatcherDef);+context.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATH_MATCHER_BEAN_NAME));}return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME);}@@ -204,70 +207,72 @@ else if (corsConfigurations != null) {* Registers an {@link HandlerMappingIntrospector} under a well-known name* unless already registered.*/-private static void registerHandlerMappingIntrospector(ParserContext parserContext, @Nullable Object source) {-if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {+private static void registerHandlerMappingIntrospector(ParserContext context, @Nullable Object source) {+if (!context.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {RootBeanDefinition beanDef = new RootBeanDefinition(HandlerMappingIntrospector.class);beanDef.setSource(source);beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);beanDef.setLazyInit(true);-parserContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef);-parserContext.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME));+context.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef);+context.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME));}}/**-* Registers an {@link FixedThemeResolver} under a well-known name+* Registers an {@link AcceptHeaderLocaleResolver} under a well-known name* unless already registered.*/-private static void registerThemeResolver(ParserContext parserContext, @Nullable Object source) {-if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME)) {-RootBeanDefinition beanDef = new RootBeanDefinition(FixedThemeResolver.class);+private static void registerLocaleResolver(ParserContext context, @Nullable Object source) {+if (!containsBeanInHierarchy(context, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)) {+RootBeanDefinition beanDef = new RootBeanDefinition(AcceptHeaderLocaleResolver.class);beanDef.setSource(source);beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);-parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME, beanDef);-parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.THEME_RESOLVER_BEAN_NAME));+context.getRegistry().registerBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, beanDef);+context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME));}}/**-* Registers an {@link AcceptHeaderLocaleResolver} under a well-known name+* Registers an {@link FixedThemeResolver} under a well-known name* unless already registered.*/-private static void registerLocaleResolver(ParserContext parserContext, @Nullable Object source) {-if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)) {-RootBeanDefinition beanDef = new RootBeanDefinition(AcceptHeaderLocaleResolver.class);+private static void registerThemeResolver(ParserContext context, @Nullable Object source) {+if (!containsBeanInHierarchy(context, DispatcherServlet.THEME_RESOLVER_BEAN_NAME)) {+RootBeanDefinition beanDef = new RootBeanDefinition(FixedThemeResolver.class);beanDef.setSource(source);beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);-parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, beanDef);-parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME));+context.getRegistry().registerBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME, beanDef);+context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.THEME_RESOLVER_BEAN_NAME));}}/**-* Registers an {@link SessionFlashMapManager} under a well-known name+* Registers an {@link DefaultRequestToViewNameTranslator} under a well-known name* unless already registered.*/-private static void registerFlashMapManager(ParserContext parserContext, @Nullable Object source) {-if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME)) {-RootBeanDefinition beanDef = new RootBeanDefinition(SessionFlashMapManager.class);+private static void registerViewNameTranslator(ParserContext context, @Nullable Object source) {+if (!containsBeanInHierarchy(context, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME)) {+RootBeanDefinition beanDef = new RootBeanDefinition(DefaultRequestToViewNameTranslator.class);beanDef.setSource(source);beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);-parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, beanDef);-parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME));+context.getRegistry().registerBeanDefinition(+DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, beanDef);+context.registerComponent(+new BeanComponentDefinition(beanDef, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME));}}/**-* Registers an {@link DefaultRequestToViewNameTranslator} under a well-known name+* Registers an {@link SessionFlashMapManager} under a well-known name* unless already registered.*/-private static void registerViewNameTranslator(ParserContext parserContext, @Nullable Object source) {-if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME)) {-RootBeanDefinition beanDef = new RootBeanDefinition(DefaultRequestToViewNameTranslator.class);+private static void registerFlashMapManager(ParserContext context, @Nullable Object source) {+if (!containsBeanInHierarchy(context, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME)) {+RootBeanDefinition beanDef = new RootBeanDefinition(SessionFlashMapManager.class);beanDef.setSource(source);beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);-parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, beanDef);-parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME));+context.getRegistry().registerBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, beanDef);+context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME));}}@@ -290,4 +295,16 @@ public static Object getContentNegotiationManager(ParserContext context) {return null;}+/**+* Check for an existing bean of the given name, ideally in the entire+* context hierarchy (through a {@code containsBean} call) since this+* is also what {@code DispatcherServlet} does, or otherwise just in+* the local context (through {@code containsBeanDefinition}).+*/+private static boolean containsBeanInHierarchy(ParserContext context, String beanName) {+BeanDefinitionRegistry registry = context.getRegistry();+return (registry instanceof BeanFactory ? ((BeanFactory) registry).containsBean(beanName) :+registry.containsBeanDefinition(beanName));+}+}
spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java+9 −8
@@ -19,14 +19,15 @@import java.lang.annotation.Annotation;import java.util.Collections;import java.util.HashSet;+import java.util.LinkedHashSet;import java.util.Set;import java.util.function.Predicate;+import java.util.stream.Collectors;import org.springframework.core.SpringProperties;import org.springframework.core.annotation.AnnotatedElementUtils;import org.springframework.core.annotation.AnnotationUtils;import org.springframework.core.annotation.MergedAnnotation;-import org.springframework.core.annotation.MergedAnnotationCollectors;import org.springframework.core.annotation.MergedAnnotationPredicates;import org.springframework.core.annotation.MergedAnnotations;import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;@@ -146,7 +147,8 @@ public static <T extends Annotation> Set<T> getMergedRepeatableAnnotations(// Present (via @Inherited semantics), directly present, or meta-present?Set<T> mergedAnnotations = MergedAnnotations.from(clazz, SearchStrategy.INHERITED_ANNOTATIONS).stream(annotationType)-.collect(MergedAnnotationCollectors.toAnnotationSet());+.map(MergedAnnotation::synthesize)+.collect(Collectors.toCollection(LinkedHashSet::new));if (!mergedAnnotations.isEmpty()) {return mergedAnnotations;@@ -545,19 +547,18 @@ public AnnotationDescriptor<T> next() {/*** Find <strong>all</strong> annotations of the specified annotation type* that are present or meta-present on the {@linkplain #getRootDeclaringClass()-* root declaring class} of this descriptor.+* root declaring class} of this descriptor or on any interfaces that the+* root declaring class implements.* @return the set of all merged, synthesized {@code Annotations} found,* or an empty set if none were found*/public Set<T> findAllLocalMergedAnnotations() {-SearchStrategy searchStrategy =-(getEnclosingConfiguration(getRootDeclaringClass()) == EnclosingConfiguration.INHERIT ?-SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES :-SearchStrategy.TYPE_HIERARCHY);+SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY;return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none()).stream(getAnnotationType()).filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))-.collect(MergedAnnotationCollectors.toAnnotationSet());+.map(MergedAnnotation::synthesize)+.collect(Collectors.toCollection(LinkedHashSet::new));}/**.../function/client/DefaultWebClient.java | 16 +++++++++.../reactive/function/client/WebClient.java | 24 +++++++++++++.../client/WebClientIntegrationTests.java | 35 ++++++++++++++++++-3 files changed, 74 insertions(+), 1 deletion(-)
More files changed — see the full commit.