Security context
Medium· 4.3GHSA-rfmp-97jj-h8m6 CVE-2021-22096CWE-117Published May 24, 2022

Improper Output Neutralization for Logs in Spring Framework

Research this vulnerability

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)

· Oct 27, 2020, 01:48 PM+1537823compare
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.adoc
src/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 instance
private 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 constructor
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
@@ -150,7 +152,7 @@ private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl e
// Create getValue() method
mv = 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() {
@Override
protected 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 test
request = 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"));
+ }
+
@Test
public 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, HttpOutputMessa
MediaType 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 mimeType
return 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 buffe
byte[] separator = getStreamingMediaTypeSeparator(mimeType);
if (separator != null) { // streaming
try {
- 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 buffe
public 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.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
/**
@@ -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)
-
@Test
fun 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.

References