From d5b5e894c95d3b90a665178b03b3fa60dd4a53b3 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Tue, 10 Nov 2020 09:02:35 +0000 Subject: [PATCH 0001/1294] Next development version (v5.3.2-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 61d8962566ab..53d0643b4525 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.1-SNAPSHOT +version=5.3.2-SNAPSHOT org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true From b019f30a152560c36f589f1e584dd81706ac0ba8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 10 Nov 2020 15:28:34 +0100 Subject: [PATCH 0002/1294] Validate that test & lifecycle methods are not @Autowired Prior to this commit, a developer may have accidentally annotated a JUnit Jupiter test method or lifecycle method with @Autowired, and that would have potentially resulted in an exception that was hard to understand. This is because the Spring container considers any @Autowired method to be a "configuration method" when autowiring the test class instance. Consequently, such an @Autowired method would be invoked twice: once by Spring while attempting to autowire the test instance and another time by JUnit Jupiter when invoking the test or lifecycle method. The autowiring invocation of the method often leads to an exception, either because Spring cannot satisfy a dependency (such as JUnit Jupiter's TestInfo) or because the body of the method fails due to test setup that has not yet been invoked. This commit introduces validation for @Autowired test and lifecycle methods in the SpringExtension that will throw an IllegalStateException if any @Autowired method in a test class is also annotated with any of the following JUnit Jupiter annotations. - @Test - @TestFactory - @TestTemplate - @RepeatedTest - @ParameterizedTest - @BeforeAll - @AfterAll - @BeforeEach - @AfterEach Closes gh-25966 --- .../junit/jupiter/SpringExtension.java | 86 +++++- ...edConfigurationErrorsIntegrationTests.java | 290 ++++++++++++++++++ 2 files changed, 372 insertions(+), 4 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index c9d185ffb04f..97b5f1428f89 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java @@ -16,11 +16,19 @@ package org.springframework.test.context.junit.jupiter; +import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; @@ -33,15 +41,22 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.platform.commons.annotation.Testable; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.ParameterResolutionDelegate; import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.annotation.RepeatableContainers; import org.springframework.lang.Nullable; import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestContextManager; import org.springframework.test.context.support.PropertyProvider; import org.springframework.test.context.support.TestConstructorUtils; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; /** * {@code SpringExtension} integrates the Spring TestContext Framework @@ -64,10 +79,29 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes ParameterResolver { /** - * {@link Namespace} in which {@code TestContextManagers} are stored, - * keyed by test class. + * {@link Namespace} in which {@code TestContextManagers} are stored, keyed + * by test class. */ - private static final Namespace NAMESPACE = Namespace.create(SpringExtension.class); + private static final Namespace TEST_CONTEXT_MANAGER_NAMESPACE = Namespace.create(SpringExtension.class); + + /** + * {@link Namespace} in which {@code @Autowired} validation error messages + * are stored, keyed by test class. + */ + private static final Namespace AUTOWIRED_VALIDATION_NAMESPACE = Namespace.create(SpringExtension.class.getName() + + "#autowired.validation"); + + private static final String NO_AUTOWIRED_VIOLATIONS_DETECTED = "NO AUTOWIRED VIOLATIONS DETECTED"; + + // Note that @Test, @TestFactory, @TestTemplate, @RepeatedTest, and @ParameterizedTest + // are all meta-annotated with @Testable. + private static final List> JUPITER_ANNOTATION_TYPES = + Arrays.asList(BeforeAll.class, AfterAll.class, BeforeEach.class, AfterEach.class, Testable.class); + + private static final MethodFilter autowiredTestOrLifecycleMethodFilter = method -> + (ReflectionUtils.USER_DECLARED_METHODS.matches(method) && + !Modifier.isPrivate(method.getModifiers()) && + isAutowiredTestOrLifecycleMethod(method)); /** @@ -93,12 +127,42 @@ public void afterAll(ExtensionContext context) throws Exception { /** * Delegates to {@link TestContextManager#prepareTestInstance}. + *

As of Spring Framework 5.3.2, this method also validates that test + * methods and test lifecycle methods are not annotated with + * {@link Autowired @Autowired}. */ @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { + validateAutowiredConfig(context); getTestContextManager(context).prepareTestInstance(testInstance); } + /** + * Validate that test methods and test lifecycle methods in the supplied + * test class are not annotated with {@link Autowired @Autowired}. + * @since 5.3.2 + */ + private void validateAutowiredConfig(ExtensionContext context) { + // We save the result in the ExtensionContext.Store so that we don't + // re-validate all methods for the same test class multiple times. + Store store = context.getStore(AUTOWIRED_VALIDATION_NAMESPACE); + String errorMessage = store.getOrComputeIfAbsent(context.getRequiredTestClass(), + testClass -> { + Method[] methodsWithErrors = + ReflectionUtils.getUniqueDeclaredMethods(testClass, autowiredTestOrLifecycleMethodFilter); + return (methodsWithErrors.length == 0 ? NO_AUTOWIRED_VIOLATIONS_DETECTED : + String.format( + "Test methods and test lifecycle methods must not be annotated with @Autowired. " + + "You should instead annotate individual method parameters with @Autowired, " + + "@Qualifier, or @Value. Offending methods in test class %s: %s", + testClass.getName(), Arrays.toString(methodsWithErrors))); + }, String.class); + + if (errorMessage != NO_AUTOWIRED_VIOLATIONS_DETECTED) { + throw new IllegalStateException(errorMessage); + } + } + /** * Delegates to {@link TestContextManager#beforeTestMethod}. */ @@ -219,7 +283,21 @@ private static TestContextManager getTestContextManager(ExtensionContext context } private static Store getStore(ExtensionContext context) { - return context.getRoot().getStore(NAMESPACE); + return context.getRoot().getStore(TEST_CONTEXT_MANAGER_NAMESPACE); + } + + private static boolean isAutowiredTestOrLifecycleMethod(Method method) { + MergedAnnotations mergedAnnotations = + MergedAnnotations.from(method, SearchStrategy.DIRECT, RepeatableContainers.none()); + if (!mergedAnnotations.isPresent(Autowired.class)) { + return false; + } + for (Class annotationType : JUPITER_ANNOTATION_TYPES) { + if (mergedAnnotations.isPresent(annotationType)) { + return true; + } + } + return false; } } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java new file mode 100644 index 000000000000..e49221470d97 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java @@ -0,0 +1,290 @@ +/* + * 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.test.context.junit.jupiter; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.testkit.engine.EngineTestKit; +import org.junit.platform.testkit.engine.Events; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.testkit.engine.EventConditions.container; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; + +/** + * Integration tests for {@link Autowired @Autowired} configuration errors in + * JUnit Jupiter test classes. + * + * @author Sam Brannen + * @since 5.3.2 + */ +class AutowiredConfigurationErrorsIntegrationTests { + + @ParameterizedTest + @ValueSource(classes = { + StaticAutowiredBeforeAllMethod.class, + StaticAutowiredAfterAllMethod.class, + AutowiredBeforeEachMethod.class, + AutowiredAfterEachMethod.class, + AutowiredTestMethod.class, + AutowiredRepeatedTestMethod.class, + AutowiredParameterizedTestMethod.class + }) + void autowiredTestMethodsTestTemplateMethodsAndLifecyleMethods(Class testClass) { + testEventsFor(testClass) + .assertStatistics(stats -> stats.started(1).succeeded(0).failed(1)) + .assertThatEvents().haveExactly(1, + event(test("test"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); + } + + /** + * A non-autowired test method should fail the same as an autowired test + * method in the same class, since Spring still should not autowire the + * autowired test method as a "configuration method" when JUnit attempts to + * execute the non-autowired test method. + */ + @Test + void autowiredAndNonAutowiredTestMethods() { + testEventsFor(AutowiredAndNonAutowiredTestMethods.class) + .assertStatistics(stats -> stats.started(2).succeeded(0).failed(2)) + .assertThatEvents() + .haveExactly(1, + event(test("autowired"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))) + .haveExactly(1, + event(test("nonAutowired"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); + } + + + @ParameterizedTest + @ValueSource(classes = { + NonStaticAutowiredBeforeAllMethod.class, + NonStaticAutowiredAfterAllMethod.class + }) + void autowiredNonStaticClassLevelLifecyleMethods(Class testClass) { + containerEventsFor(testClass) + .assertStatistics(stats -> stats.started(2).succeeded(1).failed(1)) + .assertThatEvents().haveExactly(1, + event(container(), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); + } + + @Test + void autowiredTestFactoryMethod() { + containerEventsFor(AutowiredTestFactoryMethod.class) + .assertStatistics(stats -> stats.started(3).succeeded(2).failed(1)) + .assertThatEvents().haveExactly(1, + event(container(), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); + } + + private Events testEventsFor(Class testClass) { + return EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .testEvents(); + } + + private Events containerEventsFor(Class testClass) { + return EngineTestKit.engine("junit-jupiter") + .selectors(selectClass(testClass)) + .execute() + .containerEvents(); + } + + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class StaticAutowiredBeforeAllMethod { + + @Autowired + @BeforeAll + static void beforeAll(TestInfo testInfo) { + } + + @Test + void test() { + } + } + + @SpringJUnitConfig(Config.class) + @TestInstance(PER_CLASS) + @FailingTestCase + static class NonStaticAutowiredBeforeAllMethod { + + @Autowired + @BeforeAll + void beforeAll(TestInfo testInfo) { + } + + @Test + void test() { + } + } + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class StaticAutowiredAfterAllMethod { + + @Test + void test() { + } + + @AfterAll + @Autowired + static void afterAll(TestInfo testInfo) { + } + } + + @SpringJUnitConfig(Config.class) + @TestInstance(PER_CLASS) + @FailingTestCase + static class NonStaticAutowiredAfterAllMethod { + + @Test + void test() { + } + + @AfterAll + @Autowired + void afterAll(TestInfo testInfo) { + } + } + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class AutowiredBeforeEachMethod { + + @Autowired + @BeforeEach + void beforeEach(TestInfo testInfo) { + } + + @Test + void test() { + } + } + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class AutowiredAfterEachMethod { + + @Test + void test() { + } + + @Autowired + @AfterEach + void afterEach(TestInfo testInfo) { + } + } + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class AutowiredTestMethod { + + @Autowired + @Test + void test(TestInfo testInfo) { + } + } + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class AutowiredAndNonAutowiredTestMethods { + + @Autowired + @Test + void autowired(TestInfo testInfo) { + } + + @Test + void nonAutowired(TestInfo testInfo) { + } + } + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class AutowiredRepeatedTestMethod { + + @Autowired + @RepeatedTest(1) + void repeatedTest(TestInfo testInfo) { + } + } + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class AutowiredTestFactoryMethod { + + @Autowired + @TestFactory + Stream testFactory(TestInfo testInfo) { + return Stream.of(dynamicTest("dynamicTest", () -> {})); + } + } + + @SpringJUnitConfig(Config.class) + @FailingTestCase + static class AutowiredParameterizedTestMethod { + + @Autowired + @ParameterizedTest + @ValueSource(strings = "ignored") + void parameterizedTest(TestInfo testInfo) { + } + } + + @Configuration + static class Config { + } + +} + From daf9a82e8ce3e6e8e795fcb46309cd8bc4a62e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=A6=D1=8B=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Tue, 10 Nov 2020 16:25:54 +0200 Subject: [PATCH 0003/1294] Simplify AbstractAspectJAdvice.isVariableName() --- .../aop/aspectj/AbstractAspectJAdvice.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index 32b691f4410b..1eb0e274562e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -350,13 +350,12 @@ protected Class getDiscoveredThrowingType() { return this.discoveredThrowingType; } - private boolean isVariableName(String name) { - char[] chars = name.toCharArray(); - if (!Character.isJavaIdentifierStart(chars[0])) { + private static boolean isVariableName(String name) { + if (!Character.isJavaIdentifierStart(name.charAt(0))) { return false; } - for (int i = 1; i < chars.length; i++) { - if (!Character.isJavaIdentifierPart(chars[i])) { + for (char ch: name.toCharArray()) { + if (!Character.isJavaIdentifierPart(ch)) { return false; } } From fc5e3c335f2b7901d2bdfa2d783dec4352dde906 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 10 Nov 2020 16:31:31 +0100 Subject: [PATCH 0004/1294] Introduce and() methods in MethodFilter & FieldFilter for composition This commit introduces `and()` default methods in the MethodFilter and FieldFilter functional interfaces in ReflectionUtils in order to simplify uses cases that need to compose filter logic. Closes gh-26063 --- .../springframework/util/ReflectionUtils.java | 26 ++++++++++++++++++- .../util/ReflectionUtilsTests.java | 7 +---- .../junit/jupiter/SpringExtension.java | 8 +++--- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java index 531d5976615a..e00fef835c66 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -824,6 +824,18 @@ public interface MethodFilter { * @param method the method to check */ boolean matches(Method method); + + /** + * Create a composite filter based on this filter and the provided + * filter. + *

If this filter does not match, the next filter will not be applied. + * @param next the next {@code MethodFilter} + * @return a composite {@code MethodFilter} + * @since 5.3.2 + */ + default MethodFilter and(MethodFilter next) { + return method -> matches(method) && next.matches(method); + } } @@ -852,6 +864,18 @@ public interface FieldFilter { * @param field the field to check */ boolean matches(Field field); + + /** + * Create a composite filter based on this filter and the provided + * filter. + *

If this filter does not match, the next filter will not be applied. + * @param next the next {@code FieldFilter} + * @return a composite {@code FieldFilter} + * @since 5.3.2 + */ + default FieldFilter and(FieldFilter next) { + return field -> matches(field) && next.matches(field); + } } } diff --git a/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java index 538592224654..8ca69ddc9a05 100644 --- a/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ReflectionUtilsTests.java @@ -186,12 +186,7 @@ private void testValidCopy(TestObject src, TestObject dest) { @Test void doWithProtectedMethods() { ListSavingMethodCallback mc = new ListSavingMethodCallback(); - ReflectionUtils.doWithMethods(TestObject.class, mc, new ReflectionUtils.MethodFilter() { - @Override - public boolean matches(Method m) { - return Modifier.isProtected(m.getModifiers()); - } - }); + ReflectionUtils.doWithMethods(TestObject.class, mc, method -> Modifier.isProtected(method.getModifiers())); assertThat(mc.getMethodNames().isEmpty()).isFalse(); assertThat(mc.getMethodNames().contains("clone")).as("Must find protected method on Object").isTrue(); assertThat(mc.getMethodNames().contains("finalize")).as("Must find protected method on Object").isTrue(); diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index 97b5f1428f89..cf72a2b24138 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java @@ -98,10 +98,10 @@ public class SpringExtension implements BeforeAllCallback, AfterAllCallback, Tes private static final List> JUPITER_ANNOTATION_TYPES = Arrays.asList(BeforeAll.class, AfterAll.class, BeforeEach.class, AfterEach.class, Testable.class); - private static final MethodFilter autowiredTestOrLifecycleMethodFilter = method -> - (ReflectionUtils.USER_DECLARED_METHODS.matches(method) && - !Modifier.isPrivate(method.getModifiers()) && - isAutowiredTestOrLifecycleMethod(method)); + private static final MethodFilter autowiredTestOrLifecycleMethodFilter = + ReflectionUtils.USER_DECLARED_METHODS + .and(method -> !Modifier.isPrivate(method.getModifiers())) + .and(SpringExtension::isAutowiredTestOrLifecycleMethod); /** From 2b1f229998422249eaee488777a9a9b8b04c05bd Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 10 Nov 2020 19:59:36 +0000 Subject: [PATCH 0005/1294] LimitedDataBufferList adds or raises error Closes gh-26060 --- .../core/io/buffer/LimitedDataBufferList.java | 9 +++------ .../core/io/buffer/DataBufferUtilsTests.java | 18 ++++++++++++++++++ .../io/buffer/LimitedDataBufferListTests.java | 10 +++++++--- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java index fb8c42aeeb0e..d95e426d3852 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -54,11 +54,8 @@ public LimitedDataBufferList(int maxByteCount) { @Override public boolean add(DataBuffer buffer) { - boolean result = super.add(buffer); - if (result) { - updateCount(buffer.readableByteCount()); - } - return result; + updateCount(buffer.readableByteCount()); + return super.add(buffer); } @Override diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java index 7c71dc8b7288..8615551b3193 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java @@ -35,6 +35,8 @@ import java.util.concurrent.CountDownLatch; import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; +import org.junit.jupiter.api.Test; import org.mockito.stubbing.Answer; import org.reactivestreams.Subscription; import reactor.core.publisher.BaseSubscriber; @@ -834,6 +836,22 @@ void joinWithLimit(String displayName, DataBufferFactory bufferFactory) { .verifyError(DataBufferLimitException.class); } + @Test // gh-26060 + void joinWithLimitDoesNotOverRelease() { + NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT); + byte[] bytes = "foo-bar-baz".getBytes(StandardCharsets.UTF_8); + + NettyDataBuffer buffer = bufferFactory.allocateBuffer(bytes.length); + buffer.getNativeBuffer().retain(); // should be at 2 now + buffer.write(bytes); + + Mono result = DataBufferUtils.join(Flux.just(buffer), 8); + + StepVerifier.create(result).verifyError(DataBufferLimitException.class); + assertThat(buffer.getNativeBuffer().refCnt()).isEqualTo(1); + buffer.release(); + } + @ParameterizedDataBufferAllocatingTest void joinErrors(String displayName, DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java index fa650f0125ba..971cd1212061 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java @@ -17,9 +17,11 @@ import java.nio.charset.StandardCharsets; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + /** * Unit tests for {@link LimitedDataBufferList}. * @author Rossen Stoyanchev @@ -29,8 +31,10 @@ public class LimitedDataBufferListTests { @Test void limitEnforced() { - Assertions.assertThatThrownBy(() -> new LimitedDataBufferList(5).add(toDataBuffer("123456"))) - .isInstanceOf(DataBufferLimitException.class); + LimitedDataBufferList list = new LimitedDataBufferList(5); + + assertThatThrownBy(() -> list.add(toDataBuffer("123456"))).isInstanceOf(DataBufferLimitException.class); + assertThat(list).isEmpty(); } @Test From 3851b291da0aaa293fa62c5eeeafd51bcc4a1d4f Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 11 Nov 2020 11:11:34 +0100 Subject: [PATCH 0006/1294] Use MethodFilter.and() in TransactionalTestExecutionListener --- .../transaction/TransactionalTestExecutionListener.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index ff134b3d9f7d..a64897e84161 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -22,7 +22,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -46,6 +45,7 @@ import org.springframework.transaction.interceptor.TransactionAttributeSource; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; /** @@ -462,9 +462,9 @@ protected final boolean isRollback(TestContext testContext) throws Exception { * as well as annotated interface default methods */ private List getAnnotatedMethods(Class clazz, Class annotationType) { - return Arrays.stream(ReflectionUtils.getUniqueDeclaredMethods(clazz, ReflectionUtils.USER_DECLARED_METHODS)) - .filter(method -> AnnotatedElementUtils.hasAnnotation(method, annotationType)) - .collect(Collectors.toList()); + MethodFilter methodFilter = ReflectionUtils.USER_DECLARED_METHODS + .and(method -> AnnotatedElementUtils.hasAnnotation(method, annotationType)); + return Arrays.asList(ReflectionUtils.getUniqueDeclaredMethods(clazz, methodFilter)); } } From bc32d513d96a3c5f1882d98bde09ab8106a4d514 Mon Sep 17 00:00:00 2001 From: izeye Date: Thu, 12 Nov 2020 08:21:19 +0900 Subject: [PATCH 0007/1294] Polish Javadoc for InjectionMetadata.forElements() --- .../beans/factory/annotation/InjectionMetadata.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java index f5cc0f9d5280..f7dcb8d18cf1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java @@ -141,8 +141,7 @@ public void clear(@Nullable PropertyValues pvs) { * Return an {@code InjectionMetadata} instance, possibly for empty elements. * @param elements the elements to inject (possibly empty) * @param clazz the target class - * @return a new {@link #InjectionMetadata(Class, Collection)} instance, - * or {@link #EMPTY} in case of no elements + * @return a new {@link #InjectionMetadata(Class, Collection)} instance * @since 5.2 */ public static InjectionMetadata forElements(Collection elements, Class clazz) { From 6eec1acdac84f60ef235fba2653c3a107ebd2aa3 Mon Sep 17 00:00:00 2001 From: fengyuanwei Date: Thu, 12 Nov 2020 12:23:24 +0800 Subject: [PATCH 0008/1294] Make tests meaningful in DefaultListableBeanFactoryTests --- .../DefaultListableBeanFactoryTests.java | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index dd4ad2b8f1e1..91f5a0a3f02e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -788,9 +788,10 @@ void canReferenceParentBeanFromChildViaAlias() { TestBean child = (TestBean) factory.getBean("child"); assertThat(child.getName()).isEqualTo(EXPECTED_NAME); assertThat(child.getAge()).isEqualTo(EXPECTED_AGE); + Object mergedBeanDefinition1 = factory.getMergedBeanDefinition("child"); Object mergedBeanDefinition2 = factory.getMergedBeanDefinition("child"); - assertThat(mergedBeanDefinition2).as("Use cached merged bean definition").isEqualTo(mergedBeanDefinition2); + assertThat(mergedBeanDefinition1).as("Use cached merged bean definition").isEqualTo(mergedBeanDefinition2); } @Test @@ -1838,8 +1839,7 @@ void autowireBeanWithFactoryBeanByType() { assertThat(factoryBean).as("The FactoryBean should have been registered.").isNotNull(); FactoryBeanDependentBean bean = (FactoryBeanDependentBean) lbf.autowire(FactoryBeanDependentBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); - Object mergedBeanDefinition2 = bean.getFactoryBean(); - assertThat(mergedBeanDefinition2).as("The FactoryBeanDependentBean should have been autowired 'by type' with the LazyInitFactory.").isEqualTo(mergedBeanDefinition2); + assertThat(bean.getFactoryBean()).as("The FactoryBeanDependentBean should have been autowired 'by type' with the LazyInitFactory.").isEqualTo(factoryBean); } @Test @@ -2388,8 +2388,7 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) { BeanWithDestroyMethod.closeCount = 0; lbf.preInstantiateSingletons(); lbf.destroySingletons(); - Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount; - assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(1); } @Test @@ -2403,8 +2402,7 @@ void destroyMethodOnInnerBean() { BeanWithDestroyMethod.closeCount = 0; lbf.preInstantiateSingletons(); lbf.destroySingletons(); - Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount; - assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(2); } @Test @@ -2419,8 +2417,7 @@ void destroyMethodOnInnerBeanAsPrototype() { BeanWithDestroyMethod.closeCount = 0; lbf.preInstantiateSingletons(); lbf.destroySingletons(); - Object mergedBeanDefinition2 = BeanWithDestroyMethod.closeCount; - assertThat(mergedBeanDefinition2).as("Destroy methods invoked").isEqualTo(mergedBeanDefinition2); + assertThat(BeanWithDestroyMethod.closeCount).as("Destroy methods invoked").isEqualTo(1); } @Test @@ -2542,14 +2539,15 @@ void explicitScopeInheritanceForChildBeanDefinitions() { factory.registerBeanDefinition("child", child); AbstractBeanDefinition def = (AbstractBeanDefinition) factory.getBeanDefinition("child"); - Object mergedBeanDefinition2 = def.getScope(); - assertThat(mergedBeanDefinition2).as("Child 'scope' not overriding parent scope (it must).").isEqualTo(mergedBeanDefinition2); + assertThat(def.getScope()).as("Child 'scope' not overriding parent scope (it must).").isEqualTo(theChildScope); } @Test void scopeInheritanceForChildBeanDefinitions() { + String theParentScope = "bonanza!"; + RootBeanDefinition parent = new RootBeanDefinition(); - parent.setScope("bonanza!"); + parent.setScope(theParentScope); AbstractBeanDefinition child = new ChildBeanDefinition("parent"); child.setBeanClass(TestBean.class); @@ -2559,8 +2557,7 @@ void scopeInheritanceForChildBeanDefinitions() { factory.registerBeanDefinition("child", child); BeanDefinition def = factory.getMergedBeanDefinition("child"); - Object mergedBeanDefinition2 = def.getScope(); - assertThat(mergedBeanDefinition2).as("Child 'scope' not inherited").isEqualTo(mergedBeanDefinition2); + assertThat(def.getScope()).as("Child 'scope' not inherited").isEqualTo(theParentScope); } @Test @@ -2596,15 +2593,12 @@ public boolean postProcessAfterInstantiation(Object bean, String beanName) throw }); lbf.preInstantiateSingletons(); TestBean tb = (TestBean) lbf.getBean("test"); - Object mergedBeanDefinition2 = tb.getName(); - assertThat(mergedBeanDefinition2).as("Name was set on field by IAPP").isEqualTo(mergedBeanDefinition2); + assertThat(tb.getName()).as("Name was set on field by IAPP").isEqualTo(nameSetOnField); if (!skipPropertyPopulation) { - Object mergedBeanDefinition21 = tb.getAge(); - assertThat(mergedBeanDefinition21).as("Property value still set").isEqualTo(mergedBeanDefinition21); + assertThat(tb.getAge()).as("Property value still set").isEqualTo(ageSetByPropertyValue); } else { - Object mergedBeanDefinition21 = tb.getAge(); - assertThat(mergedBeanDefinition21).as("Property value was NOT set and still has default value").isEqualTo(mergedBeanDefinition21); + assertThat(tb.getAge()).as("Property value was NOT set and still has default value").isEqualTo(0); } } From 66292cd7a1697a8d99b3dd3eaff4706e4beab558 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 12 Nov 2020 14:05:31 +0100 Subject: [PATCH 0009/1294] Individually apply the SQL type from each SqlParameterSource argument Closes gh-26071 --- .../core/PreparedStatementCreatorFactory.java | 4 +--- .../jdbc/core/namedparam/NamedParameterUtils.java | 4 ++-- .../core/namedparam/SqlParameterSourceUtils.java | 8 ++------ .../NamedParameterJdbcTemplateTests.java | 15 +++++++++------ 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java index f07ee04cd0a9..e6083f8be0f5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java @@ -30,7 +30,6 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * Helper class that efficiently creates multiple {@link PreparedStatementCreator} @@ -200,9 +199,8 @@ public PreparedStatementCreatorImpl(List parameters) { public PreparedStatementCreatorImpl(String actualSql, List parameters) { this.actualSql = actualSql; - Assert.notNull(parameters, "Parameters List must not be null"); this.parameters = parameters; - if (this.parameters.size() != declaredParameters.size()) { + if (parameters.size() != declaredParameters.size()) { // Account for named parameters being used multiple times Set names = new HashSet<>(); for (int i = 0; i < parameters.size(); i++) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java index 23feefd67385..4d4c414ea7a4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java @@ -345,9 +345,9 @@ public static Object[] buildValueArray( for (int i = 0; i < paramNames.size(); i++) { String paramName = paramNames.get(i); try { - Object value = paramSource.getValue(paramName); SqlParameter param = findParameter(declaredParams, paramName, i); - paramArray[i] = (param != null ? new SqlParameterValue(param, value) : value); + paramArray[i] = (param != null ? new SqlParameterValue(param, paramSource.getValue(paramName)) : + SqlParameterSourceUtils.getTypedValue(paramSource, paramName)); } catch (IllegalArgumentException ex) { throw new InvalidDataAccessApiUsageException( diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java index 4ae12a9533ad..e2bd60e05fff 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java @@ -92,17 +92,13 @@ public static SqlParameterSource[] createBatch(Map[] valueMaps) { * @param source the source of parameter values and type information * @param parameterName the name of the parameter * @return the value object + * @see SqlParameterValue */ @Nullable public static Object getTypedValue(SqlParameterSource source, String parameterName) { int sqlType = source.getSqlType(parameterName); if (sqlType != SqlParameterSource.TYPE_UNKNOWN) { - if (source.getTypeName(parameterName) != null) { - return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName)); - } - else { - return new SqlParameterValue(sqlType, source.getValue(parameterName)); - } + return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName)); } else { return source.getValue(parameterName); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java index d9dc25f77af3..31fa105d0059 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java @@ -561,10 +561,11 @@ public void testBatchUpdateWithInClause() throws Exception { @Test public void testBatchUpdateWithSqlParameterSourcePlusTypeInfo() throws Exception { - SqlParameterSource[] ids = new SqlParameterSource[2]; - ids[0] = new MapSqlParameterSource().addValue("id", 100, Types.NUMERIC); - ids[1] = new MapSqlParameterSource().addValue("id", 200, Types.NUMERIC); - final int[] rowsAffected = new int[] {1, 2}; + SqlParameterSource[] ids = new SqlParameterSource[3]; + ids[0] = new MapSqlParameterSource().addValue("id", null, Types.NULL); + ids[1] = new MapSqlParameterSource().addValue("id", 100, Types.NUMERIC); + ids[2] = new MapSqlParameterSource().addValue("id", 200, Types.NUMERIC); + final int[] rowsAffected = new int[] {1, 2, 3}; given(preparedStatement.executeBatch()).willReturn(rowsAffected); given(connection.getMetaData()).willReturn(databaseMetaData); @@ -572,13 +573,15 @@ public void testBatchUpdateWithSqlParameterSourcePlusTypeInfo() throws Exception int[] actualRowsAffected = namedParameterTemplate.batchUpdate( "UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = :id", ids); - assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue(); + assertThat(actualRowsAffected.length == 3).as("executed 3 updates").isTrue(); assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]); assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]); + assertThat(actualRowsAffected[2]).isEqualTo(rowsAffected[2]); verify(connection).prepareStatement("UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?"); + verify(preparedStatement).setNull(1, Types.NULL); verify(preparedStatement).setObject(1, 100, Types.NUMERIC); verify(preparedStatement).setObject(1, 200, Types.NUMERIC); - verify(preparedStatement, times(2)).addBatch(); + verify(preparedStatement, times(3)).addBatch(); verify(preparedStatement, atLeastOnce()).close(); verify(connection, atLeastOnce()).close(); } From c419ea7ba76596beb5d548f5cc922947ab524b9b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 12 Nov 2020 14:35:28 +0100 Subject: [PATCH 0010/1294] Use MethodFilter.and() in ReflectiveAspectJAdvisorFactory --- .../ReflectiveAspectJAdvisorFactory.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java index 5355b2bbb379..c1c10c946ed6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/ReflectiveAspectJAdvisorFactory.java @@ -51,6 +51,7 @@ import org.springframework.core.convert.converter.ConvertingComparator; import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; import org.springframework.util.comparator.InstanceComparator; @@ -70,7 +71,11 @@ @SuppressWarnings("serial") public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable { - private static final Comparator METHOD_COMPARATOR; + // Exclude @Pointcut methods + private static final MethodFilter adviceMethodFilter = ReflectionUtils.USER_DECLARED_METHODS + .and(method -> (AnnotationUtils.getAnnotation(method, Pointcut.class) == null)); + + private static final Comparator adviceMethodComparator; static { // Note: although @After is ordered before @AfterReturning and @AfterThrowing, @@ -86,7 +91,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto return (ann != null ? ann.getAnnotation() : null); }); Comparator methodNameComparator = new ConvertingComparator<>(Method::getName); - METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator); + adviceMethodComparator = adviceKindComparator.thenComparing(methodNameComparator); } @@ -160,15 +165,10 @@ public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstan } private List getAdvisorMethods(Class aspectClass) { - final List methods = new ArrayList<>(); - ReflectionUtils.doWithMethods(aspectClass, method -> { - // Exclude pointcuts - if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) { - methods.add(method); - } - }, ReflectionUtils.USER_DECLARED_METHODS); + List methods = new ArrayList<>(); + ReflectionUtils.doWithMethods(aspectClass, methods::add, adviceMethodFilter); if (methods.size() > 1) { - methods.sort(METHOD_COMPARATOR); + methods.sort(adviceMethodComparator); } return methods; } From bd4e915abfdfc43e37e719d079db06144303be81 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 12 Nov 2020 14:57:05 +0100 Subject: [PATCH 0011/1294] Assert same instance returned for cached merged BeanDefinition --- .../beans/factory/DefaultListableBeanFactoryTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index 91f5a0a3f02e..13321a7e9a2e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -785,13 +785,13 @@ void canReferenceParentBeanFromChildViaAlias() { factory.registerBeanDefinition("child", childDefinition); factory.registerAlias("parent", "alias"); - TestBean child = (TestBean) factory.getBean("child"); + TestBean child = factory.getBean("child", TestBean.class); assertThat(child.getName()).isEqualTo(EXPECTED_NAME); assertThat(child.getAge()).isEqualTo(EXPECTED_AGE); - Object mergedBeanDefinition1 = factory.getMergedBeanDefinition("child"); - Object mergedBeanDefinition2 = factory.getMergedBeanDefinition("child"); + BeanDefinition mergedBeanDefinition1 = factory.getMergedBeanDefinition("child"); + BeanDefinition mergedBeanDefinition2 = factory.getMergedBeanDefinition("child"); - assertThat(mergedBeanDefinition1).as("Use cached merged bean definition").isEqualTo(mergedBeanDefinition2); + assertThat(mergedBeanDefinition1).as("Use cached merged bean definition").isSameAs(mergedBeanDefinition2); } @Test From 48af36c6fa7408e917cc837734499293ea2f96ad Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 12 Nov 2020 15:11:54 +0100 Subject: [PATCH 0012/1294] Ensure test() conditions in JUnit TestKit match method names Prior to this commit, the test("test") conditions used in AutowiredConfigurationErrorsIntegrationTests inadvertently asserted that the invoked test methods reside in an org.springframework.test subpackage, which is always the case for any test method in the `spring-test` module. In other words, "test" is always a substring of "org.springframework.test...", which is not a meaningful assertion. This commit ensures that the JUnit Platform Test Kit is asserting the actual names of test methods. --- .../AutowiredConfigurationErrorsIntegrationTests.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java index e49221470d97..35dd5e8f5e25 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java @@ -69,7 +69,7 @@ void autowiredTestMethodsTestTemplateMethodsAndLifecyleMethods(Class testClas testEventsFor(testClass) .assertStatistics(stats -> stats.started(1).succeeded(0).failed(1)) .assertThatEvents().haveExactly(1, - event(test("test"), + event(test("test("), finishedWithFailure( instanceOf(IllegalStateException.class), message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); @@ -87,12 +87,12 @@ void autowiredAndNonAutowiredTestMethods() { .assertStatistics(stats -> stats.started(2).succeeded(0).failed(2)) .assertThatEvents() .haveExactly(1, - event(test("autowired"), + event(test("autowired(org.junit.jupiter.api.TestInfo)"), finishedWithFailure( instanceOf(IllegalStateException.class), message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))) .haveExactly(1, - event(test("nonAutowired"), + event(test("nonAutowired(org.junit.jupiter.api.TestInfo)"), finishedWithFailure( instanceOf(IllegalStateException.class), message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); @@ -256,7 +256,7 @@ static class AutowiredRepeatedTestMethod { @Autowired @RepeatedTest(1) - void repeatedTest(TestInfo testInfo) { + void test(TestInfo testInfo) { } } @@ -278,7 +278,7 @@ static class AutowiredParameterizedTestMethod { @Autowired @ParameterizedTest @ValueSource(strings = "ignored") - void parameterizedTest(TestInfo testInfo) { + void test(TestInfo testInfo) { } } From 1b0be5862daf5c8380378c9d68a65a6f234757d4 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 12 Nov 2020 15:32:41 +0100 Subject: [PATCH 0013/1294] Polishing --- ...edConfigurationErrorsIntegrationTests.java | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java index 35dd5e8f5e25..fc877f12a708 100644 --- a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/AutowiredConfigurationErrorsIntegrationTests.java @@ -18,10 +18,12 @@ import java.util.stream.Stream; +import org.assertj.core.api.Condition; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; @@ -31,18 +33,20 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.testkit.engine.EngineTestKit; +import org.junit.platform.testkit.engine.Event; +import org.junit.platform.testkit.engine.EventConditions; import org.junit.platform.testkit.engine.Events; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import static org.assertj.core.api.Assertions.allOf; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.testkit.engine.EventConditions.container; import static org.junit.platform.testkit.engine.EventConditions.event; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; -import static org.junit.platform.testkit.engine.EventConditions.test; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; @@ -55,6 +59,9 @@ */ class AutowiredConfigurationErrorsIntegrationTests { + private static final String DISPLAY_NAME = "TEST"; + + @ParameterizedTest @ValueSource(classes = { StaticAutowiredBeforeAllMethod.class, @@ -69,7 +76,7 @@ void autowiredTestMethodsTestTemplateMethodsAndLifecyleMethods(Class testClas testEventsFor(testClass) .assertStatistics(stats -> stats.started(1).succeeded(0).failed(1)) .assertThatEvents().haveExactly(1, - event(test("test("), + event(testWithDisplayName(DISPLAY_NAME), finishedWithFailure( instanceOf(IllegalStateException.class), message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); @@ -84,18 +91,18 @@ void autowiredTestMethodsTestTemplateMethodsAndLifecyleMethods(Class testClas @Test void autowiredAndNonAutowiredTestMethods() { testEventsFor(AutowiredAndNonAutowiredTestMethods.class) - .assertStatistics(stats -> stats.started(2).succeeded(0).failed(2)) - .assertThatEvents() - .haveExactly(1, - event(test("autowired(org.junit.jupiter.api.TestInfo)"), - finishedWithFailure( - instanceOf(IllegalStateException.class), - message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))) - .haveExactly(1, - event(test("nonAutowired(org.junit.jupiter.api.TestInfo)"), - finishedWithFailure( - instanceOf(IllegalStateException.class), - message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); + .assertStatistics(stats -> stats.started(2).succeeded(0).failed(2)) + .assertThatEvents() + .haveExactly(1, + event(testWithDisplayName("autowired(TestInfo)"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))) + .haveExactly(1, + event(testWithDisplayName("nonAutowired(TestInfo)"), + finishedWithFailure( + instanceOf(IllegalStateException.class), + message(msg -> msg.matches(".+must not be annotated with @Autowired.+"))))); } @@ -139,6 +146,10 @@ private Events containerEventsFor(Class testClass) { .containerEvents(); } + private static Condition testWithDisplayName(String displayName) { + return allOf(EventConditions.test(), EventConditions.displayName(displayName)); + } + @SpringJUnitConfig(Config.class) @FailingTestCase @@ -150,6 +161,7 @@ static void beforeAll(TestInfo testInfo) { } @Test + @DisplayName(DISPLAY_NAME) void test() { } } @@ -165,6 +177,7 @@ void beforeAll(TestInfo testInfo) { } @Test + @DisplayName(DISPLAY_NAME) void test() { } } @@ -174,6 +187,7 @@ void test() { static class StaticAutowiredAfterAllMethod { @Test + @DisplayName(DISPLAY_NAME) void test() { } @@ -189,6 +203,7 @@ static void afterAll(TestInfo testInfo) { static class NonStaticAutowiredAfterAllMethod { @Test + @DisplayName(DISPLAY_NAME) void test() { } @@ -208,6 +223,7 @@ void beforeEach(TestInfo testInfo) { } @Test + @DisplayName(DISPLAY_NAME) void test() { } } @@ -217,6 +233,7 @@ void test() { static class AutowiredAfterEachMethod { @Test + @DisplayName(DISPLAY_NAME) void test() { } @@ -232,6 +249,7 @@ static class AutowiredTestMethod { @Autowired @Test + @DisplayName(DISPLAY_NAME) void test(TestInfo testInfo) { } } @@ -255,7 +273,7 @@ void nonAutowired(TestInfo testInfo) { static class AutowiredRepeatedTestMethod { @Autowired - @RepeatedTest(1) + @RepeatedTest(value = 1, name = DISPLAY_NAME) void test(TestInfo testInfo) { } } @@ -276,7 +294,7 @@ Stream testFactory(TestInfo testInfo) { static class AutowiredParameterizedTestMethod { @Autowired - @ParameterizedTest + @ParameterizedTest(name = DISPLAY_NAME) @ValueSource(strings = "ignored") void test(TestInfo testInfo) { } From 5338d8b5e90acf43a919b8b1eb1803748a3f81c7 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 11 Nov 2020 14:02:08 +0100 Subject: [PATCH 0014/1294] Add cron expression documentation This commit adds a section about the cron expression format supported by Spring. Closes gh-26067 --- src/docs/asciidoc/integration.adoc | 98 ++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 4 deletions(-) diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index 7b1a54a5f95c..1115328d8e1a 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -4948,8 +4948,9 @@ default). The following listing shows the available methods for `Trigger` implem ==== `Trigger` Implementations Spring provides two implementations of the `Trigger` interface. The most interesting one -is the `CronTrigger`. It enables the scheduling of tasks based on cron expressions. For -example, the following task is scheduled to run 15 minutes past each hour but only +is the `CronTrigger`. It enables the scheduling of tasks based on +<>. +For example, the following task is scheduled to run 15 minutes past each hour but only during the 9-to-5 "`business hours`" on weekdays: [source,java,indent=0] @@ -5087,7 +5088,8 @@ number of milliseconds to wait before the first execution of the method, as the } ---- -If simple periodic scheduling is not expressive enough, you can provide a cron expression. +If simple periodic scheduling is not expressive enough, you can provide a +<>. The following example runs only on weekdays: [source,java,indent=0] @@ -5413,7 +5415,8 @@ milliseconds to wait after each task execution has completed. Another option is `fixed-rate`, indicating how often the method should be run regardless of how long any previous execution takes. Additionally, for both `fixed-delay` and `fixed-rate` tasks, you can specify an 'initial-delay' parameter, indicating the number of milliseconds to wait -before the first execution of the method. For more control, you can instead provide a `cron` attribute. +before the first execution of the method. For more control, you can instead provide a `cron` attribute +to provide a <>. The following example shows these other options: [source,xml,indent=0] @@ -5430,6 +5433,93 @@ The following example shows these other options: +[[scheduling-cron-expression]] +=== Cron Expressions + +All Spring cron expressions have to conform to the same format, whether you are using them in +<>, +<>, +or someplace else. +A well-formed cron expression, such as `* * * * * *`, consists of six space-separated time and date +fields, each with its own range of valid values: + + +.... + ┌───────────── second (0-59) + │ ┌───────────── minute (0 - 59) + │ │ ┌───────────── hour (0 - 23) + │ │ │ ┌───────────── day of the month (1 - 31) + │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC) + │ │ │ │ │ ┌───────────── day of the week (0 - 7) + │ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN) + │ │ │ │ │ │ + * * * * * * +.... + +There are some rules that apply: + +* A field may be an asterisk (`*`), which always stands for "`first-last`". +For the day-of-the-month or day-of-the-week fields, a question mark (`?`) may be used instead of an +asterisk. +* Commas (`,`) are used to separate items of a list. +* Two numbers separated with a hyphen (`-`) express a range of numbers. +The specified range is inclusive. +* Following a range (or `*`) with `/` specifies the interval of the number's value through the range. +* English names can also be used for the day-of-month and day-of-week fields. +Use the first three letters of the particular day or month (case does not matter). +* The day-of-month and day-of-week fields can contain a `L` character, which has a different meaning +** In the day-of-month field, `L` stands for _the last day of the month_. +If followed by a negative offset (that is, `L-n`), it means _``n``th-to-last day of the month_. +** In the day-of-week field, `L` stands for _the last day of the week_. +If prefixed by a number or three-letter name (`dL` or `DDDL`), it means _the last day of week (`d` +or `DDD`) in the month_. +* The day-of-month field can be `nW`, which stands for _the nearest weekday to day of the month ``n``_. +If `n` falls on Saturday, this yields the Friday before it. +If `n` falls on Sunday, this yields the Monday after, which also happens if `n` is `1` and falls on +a Saturday (that is: `1W` stands for _the first weekday of the month_). +* If the day-of-month field is `LW`, it means _the last weekday of the month_. +* The day-of-week field can be `d#n` (or `DDD#n`), which stands for _the ``n``th day of week `d` +(or ``DDD``) in the month_. + +Here are some examples: + +|=== +| Cron Expression | Meaning + +|`0 0 * * * *` | top of every hour of every day +|`*/10 * * * * *` | every ten seconds +| `0 0 8-10 * * *` | 8, 9 and 10 o'clock of every day +| `0 0 6,19 * * *` | 6:00 AM and 7:00 PM every day +| `0 0/30 8-10 * * *` | 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day +| `0 0 9-17 * * MON-FRI`| on the hour nine-to-five weekdays +| `0 0 0 25 DEC ?` | every Christmas Day at midnight +| `0 0 0 L * *` | last day of the month at midnight +| `0 0 0 L-3 * *` | third-to-last day of the month at midnight +| `0 0 0 * * 5L` | last Friday of the month at midnight +| `0 0 0 * * THUL` | last Thursday of the month at midnight +| `0 0 0 1W * *` | first weekday of the month at midnight +| `0 0 0 LW * *` | last weekday of the month at midnight +| `0 0 0 ? * 5#2` | the second Friday in the month at midnight +| `0 0 0 ? * MON#1` | the first Monday in the month at midnight +|=== + +==== Macros + +Expressions such as `0 0 * * * *` are hard for humans to parse and are, therefore, hard to fix in case of bugs. +To improve readability, Spring supports the following macros, which represent commonly used sequences. +You can use these macros instead of the six-digit value, thus: `@Scheduled(cron = "@hourly")`. + +|=== +|Macro | Meaning + +| `@yearly` (or `@annually`) | once a year (`0 0 0 1 1 *`) +| `@monthly` | once a month (`0 0 0 1 * *`) +| `@weekly` | once a week (`0 0 0 * * 0`) +| `@daily` (or `@midnight`) | once a day (`0 0 0 * * *`), or +| `@hourly` | once an hour, (`0 0 * * * *`) +|=== + + [[scheduling-quartz]] === Using the Quartz Scheduler From ba9325446ce6a7f5863883e9ce344041c7d40168 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Nov 2020 18:36:31 +0000 Subject: [PATCH 0015/1294] Optimize WebClientUtils Use constant Predicate for exception wrapping. Use ResponseEntity constructor instead of builder. See gh-26069 --- .../springframework/http/ResponseEntity.java | 15 ++++++++-- .../function/client/DefaultWebClient.java | 30 +++++-------------- .../function/client/ExchangeFunctions.java | 2 +- .../function/client/WebClientUtils.java | 27 +++++++++-------- 4 files changed, 35 insertions(+), 39 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java index 591b56606f9c..3cd5b81edc40 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java @@ -113,9 +113,18 @@ public ResponseEntity(MultiValueMap headers, HttpStatus status) * @param status the status code */ public ResponseEntity(@Nullable T body, @Nullable MultiValueMap headers, HttpStatus status) { - super(body, headers); - Assert.notNull(status, "HttpStatus must not be null"); - this.status = status; + this(body, headers, (Object) status); + } + + /** + * Create a new {@code HttpEntity} with the given body, headers, and status code. + * @param body the entity body + * @param headers the entity headers + * @param rawStatus the status code value + * @since 5.3.2 + */ + public ResponseEntity(@Nullable T body, @Nullable MultiValueMap headers, int rawStatus) { + this(body, headers, (Object) rawStatus); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 659bf8173425..f45841c7f31c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -543,16 +543,10 @@ public Mono bodyToMono(ParameterizedTypeReference elementTypeRef) { return this.responseMono.flatMap(response -> handleBodyMono(response, response.bodyToMono(elementTypeRef))); } - private Mono handleBodyMono(ClientResponse response, Mono bodyPublisher) { + private Mono handleBodyMono(ClientResponse response, Mono body) { + body = body.onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, exceptionWrappingFunction(response)); Mono result = statusHandlers(response); - Mono wrappedExceptions = bodyPublisher.onErrorResume(WebClientUtils::shouldWrapException, - t -> wrapException(t, response)); - if (result != null) { - return result.switchIfEmpty(wrappedExceptions); - } - else { - return wrappedExceptions; - } + return (result != null ? result.switchIfEmpty(body) : body); } @Override @@ -567,16 +561,10 @@ public Flux bodyToFlux(ParameterizedTypeReference elementTypeRef) { return this.responseMono.flatMapMany(response -> handleBodyFlux(response, response.bodyToFlux(elementTypeRef))); } - private Publisher handleBodyFlux(ClientResponse response, Flux bodyPublisher) { + private Publisher handleBodyFlux(ClientResponse response, Flux body) { + body = body.onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, exceptionWrappingFunction(response)); Mono result = statusHandlers(response); - Flux wrappedExceptions = bodyPublisher.onErrorResume(WebClientUtils::shouldWrapException, - t -> wrapException(t, response)); - if (result != null) { - return result.flux().switchIfEmpty(wrappedExceptions); - } - else { - return wrappedExceptions; - } + return (result != null ? result.flux().switchIfEmpty(body) : body); } @Nullable @@ -608,10 +596,8 @@ private Mono insertCheckpoint(Mono result, int statusCode, HttpRequest return result.checkpoint(description); } - private Mono wrapException(Throwable throwable, ClientResponse response) { - return response.createException() - .map(responseException -> responseException.initCause(throwable)) - .flatMap(Mono::error); + private Function> exceptionWrappingFunction(ClientResponse response) { + return t -> response.createException().flatMap(ex -> Mono.error(ex.initCause(t))); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java index 8566a1bad8ca..d06f6bda3d99 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ExchangeFunctions.java @@ -104,7 +104,7 @@ public Mono exchange(ClientRequest clientRequest) { .connect(httpMethod, url, httpRequest -> clientRequest.writeTo(httpRequest, this.strategies)) .doOnRequest(n -> logRequest(clientRequest)) .doOnCancel(() -> logger.debug(logPrefix + "Cancel signal (to close connection)")) - .onErrorResume(WebClientUtils::shouldWrapException, t -> wrapException(t, clientRequest)) + .onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, t -> wrapException(t, clientRequest)) .map(httpResponse -> { logResponse(httpResponse, logPrefix); return new DefaultClientResponse( diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java index 799ef5bf792e..c234e0e57801 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientUtils.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.function.client; import java.util.List; +import java.util.function.Predicate; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -26,7 +27,8 @@ import org.springframework.http.ResponseEntity; /** - * Internal methods shared between {@link DefaultWebClient} and {@link DefaultClientResponse}. + * Internal methods shared between {@link DefaultWebClient} and + * {@link DefaultClientResponse}. * * @author Arjen Poutsma * @since 5.2 @@ -35,6 +37,12 @@ abstract class WebClientUtils { private static final String VALUE_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; + /** + * Predicate that returns true if an exception should be wrapped. + */ + public final static Predicate WRAP_EXCEPTION_PREDICATE = + t -> !(t instanceof WebClientException) && !(t instanceof CodecException); + /** * Map the given response to a single value {@code ResponseEntity}. @@ -42,9 +50,10 @@ abstract class WebClientUtils { @SuppressWarnings("unchecked") public static Mono> mapToEntity(ClientResponse response, Mono bodyMono) { return ((Mono) bodyMono).defaultIfEmpty(VALUE_NONE).map(body -> - ResponseEntity.status(response.rawStatusCode()) - .headers(response.headers().asHttpHeaders()) - .body(body != VALUE_NONE ? (T) body : null)); + new ResponseEntity<>( + body != VALUE_NONE ? (T) body : null, + response.headers().asHttpHeaders(), + response.rawStatusCode())); } /** @@ -52,15 +61,7 @@ public static Mono> mapToEntity(ClientResponse response, M */ public static Mono>> mapToEntityList(ClientResponse response, Publisher body) { return Flux.from(body).collectList().map(list -> - ResponseEntity.status(response.rawStatusCode()) - .headers(response.headers().asHttpHeaders()) - .body(list)); + new ResponseEntity<>(list, response.headers().asHttpHeaders(), response.rawStatusCode())); } - /** - * Indicates whether the given exception should be wrapped. - */ - public static boolean shouldWrapException(Throwable t) { - return !(t instanceof WebClientException) && !(t instanceof CodecException); - } } From 94fcb37d30b140ded9e93231606696d47ba32dcc Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Nov 2020 18:16:58 +0000 Subject: [PATCH 0016/1294] Re-order methods in DefaultResponseSpec See gh-26069 --- .../function/client/DefaultWebClient.java | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index f45841c7f31c..1ecc3858af90 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -543,12 +543,6 @@ public Mono bodyToMono(ParameterizedTypeReference elementTypeRef) { return this.responseMono.flatMap(response -> handleBodyMono(response, response.bodyToMono(elementTypeRef))); } - private Mono handleBodyMono(ClientResponse response, Mono body) { - body = body.onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, exceptionWrappingFunction(response)); - Mono result = statusHandlers(response); - return (result != null ? result.switchIfEmpty(body) : body); - } - @Override public Flux bodyToFlux(Class elementClass) { Assert.notNull(elementClass, "Class must not be null"); @@ -561,45 +555,6 @@ public Flux bodyToFlux(ParameterizedTypeReference elementTypeRef) { return this.responseMono.flatMapMany(response -> handleBodyFlux(response, response.bodyToFlux(elementTypeRef))); } - private Publisher handleBodyFlux(ClientResponse response, Flux body) { - body = body.onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, exceptionWrappingFunction(response)); - Mono result = statusHandlers(response); - return (result != null ? result.flux().switchIfEmpty(body) : body); - } - - @Nullable - private Mono statusHandlers(ClientResponse response) { - int statusCode = response.rawStatusCode(); - for (StatusHandler handler : this.statusHandlers) { - if (handler.test(statusCode)) { - Mono exMono; - try { - exMono = handler.apply(response); - exMono = exMono.flatMap(ex -> releaseIfNotConsumed(response, ex)); - exMono = exMono.onErrorResume(ex -> releaseIfNotConsumed(response, ex)); - } - catch (Throwable ex2) { - exMono = releaseIfNotConsumed(response, ex2); - } - Mono result = exMono.flatMap(Mono::error); - HttpRequest request = this.requestSupplier.get(); - return insertCheckpoint(result, statusCode, request); - } - } - return null; - } - - private Mono insertCheckpoint(Mono result, int statusCode, HttpRequest request) { - String httpMethod = request.getMethodValue(); - URI uri = request.getURI(); - String description = statusCode + " from " + httpMethod + " " + uri + " [DefaultWebClient]"; - return result.checkpoint(description); - } - - private Function> exceptionWrappingFunction(ClientResponse response) { - return t -> response.createException().flatMap(ex -> Mono.error(ex.initCause(t))); - } - @Override public Mono> toEntity(Class bodyClass) { return this.responseMono.flatMap(response -> @@ -652,6 +607,51 @@ public Mono> toBodilessEntity() { ); } + private Mono handleBodyMono(ClientResponse response, Mono body) { + body = body.onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, exceptionWrappingFunction(response)); + Mono result = applyStatusHandlers(response); + return (result != null ? result.switchIfEmpty(body) : body); + } + + private Publisher handleBodyFlux(ClientResponse response, Flux body) { + body = body.onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, exceptionWrappingFunction(response)); + Mono result = applyStatusHandlers(response); + return (result != null ? result.flux().switchIfEmpty(body) : body); + } + + private Function> exceptionWrappingFunction(ClientResponse response) { + return t -> response.createException().flatMap(ex -> Mono.error(ex.initCause(t))); + } + + @Nullable + private Mono applyStatusHandlers(ClientResponse response) { + int statusCode = response.rawStatusCode(); + for (StatusHandler handler : this.statusHandlers) { + if (handler.test(statusCode)) { + Mono exMono; + try { + exMono = handler.apply(response); + exMono = exMono.flatMap(ex -> releaseIfNotConsumed(response, ex)); + exMono = exMono.onErrorResume(ex -> releaseIfNotConsumed(response, ex)); + } + catch (Throwable ex2) { + exMono = releaseIfNotConsumed(response, ex2); + } + Mono result = exMono.flatMap(Mono::error); + HttpRequest request = this.requestSupplier.get(); + return insertCheckpoint(result, statusCode, request); + } + } + return null; + } + + private Mono insertCheckpoint(Mono result, int statusCode, HttpRequest request) { + String httpMethod = request.getMethodValue(); + URI uri = request.getURI(); + String description = statusCode + " from " + httpMethod + " " + uri + " [DefaultWebClient]"; + return result.checkpoint(description); + } + private static class StatusHandler { From 42d3bc47c9c8a86e9702604afb1a46775a1a3f14 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Nov 2020 18:42:21 +0000 Subject: [PATCH 0017/1294] toEntityFlux methods apply error status handling Closes gh-26069 --- .../function/client/DefaultWebClient.java | 36 ++++++++++++------- .../client/DefaultWebClientTests.java | 23 ++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 1ecc3858af90..db8b511927c9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -534,25 +534,29 @@ public ResponseSpec onRawStatus(IntPredicate statusCodePredicate, @Override public Mono bodyToMono(Class elementClass) { Assert.notNull(elementClass, "Class must not be null"); - return this.responseMono.flatMap(response -> handleBodyMono(response, response.bodyToMono(elementClass))); + return this.responseMono.flatMap(response -> + handleBodyMono(response, response.bodyToMono(elementClass))); } @Override public Mono bodyToMono(ParameterizedTypeReference elementTypeRef) { Assert.notNull(elementTypeRef, "ParameterizedTypeReference must not be null"); - return this.responseMono.flatMap(response -> handleBodyMono(response, response.bodyToMono(elementTypeRef))); + return this.responseMono.flatMap(response -> + handleBodyMono(response, response.bodyToMono(elementTypeRef))); } @Override public Flux bodyToFlux(Class elementClass) { Assert.notNull(elementClass, "Class must not be null"); - return this.responseMono.flatMapMany(response -> handleBodyFlux(response, response.bodyToFlux(elementClass))); + return this.responseMono.flatMapMany(response -> + handleBodyFlux(response, response.bodyToFlux(elementClass))); } @Override public Flux bodyToFlux(ParameterizedTypeReference elementTypeRef) { Assert.notNull(elementTypeRef, "ParameterizedTypeReference must not be null"); - return this.responseMono.flatMapMany(response -> handleBodyFlux(response, response.bodyToFlux(elementTypeRef))); + return this.responseMono.flatMapMany(response -> + handleBodyFlux(response, response.bodyToFlux(elementTypeRef))); } @Override @@ -585,18 +589,14 @@ public Mono>> toEntityList(ParameterizedTypeReference @Override public Mono>> toEntityFlux(Class elementType) { - return this.responseMono.map(response -> - ResponseEntity.status(response.rawStatusCode()) - .headers(response.headers().asHttpHeaders()) - .body(response.bodyToFlux(elementType))); + return this.responseMono.flatMap(response -> + handlerEntityFlux(response, response.bodyToFlux(elementType))); } @Override - public Mono>> toEntityFlux(ParameterizedTypeReference elementTypeReference) { - return this.responseMono.map(response -> - ResponseEntity.status(response.rawStatusCode()) - .headers(response.headers().asHttpHeaders()) - .body(response.bodyToFlux(elementTypeReference))); + public Mono>> toEntityFlux(ParameterizedTypeReference elementTypeRef) { + return this.responseMono.flatMap(response -> + handlerEntityFlux(response, response.bodyToFlux(elementTypeRef))); } @Override @@ -619,6 +619,16 @@ private Publisher handleBodyFlux(ClientResponse response, Flux body) { return (result != null ? result.flux().switchIfEmpty(body) : body); } + private Mono>> handlerEntityFlux(ClientResponse response, Flux body) { + ResponseEntity> entity = new ResponseEntity<>( + body.onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, exceptionWrappingFunction(response)), + response.headers().asHttpHeaders(), + response.rawStatusCode()); + + Mono>> result = applyStatusHandlers(response); + return (result != null ? result.defaultIfEmpty(entity) : Mono.just(entity)); + } + private Function> exceptionWrappingFunction(ClientResponse response) { return t -> response.createException().flatMap(ex -> Mono.error(ex.initCause(t))); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 8e2a058fd38c..ebc6f823001a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -30,10 +30,12 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.core.NamedThreadLocal; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -443,6 +445,27 @@ public void onStatusHandlersDefaultHandlerIsLast() { verify(predicate2).test(HttpStatus.BAD_REQUEST); } + @Test // gh-26069 + public void onStatusHandlersApplyForToEntityMethods() { + + ClientResponse response = ClientResponse.create(HttpStatus.BAD_REQUEST).build(); + given(exchangeFunction.exchange(any())).willReturn(Mono.just(response)); + + WebClient.ResponseSpec spec = this.builder.build().get().uri("/path").retrieve(); + + testStatusHandlerForToEntity(spec.toEntity(String.class)); + testStatusHandlerForToEntity(spec.toEntity(new ParameterizedTypeReference() {})); + testStatusHandlerForToEntity(spec.toEntityList(String.class)); + testStatusHandlerForToEntity(spec.toEntityList(new ParameterizedTypeReference() {})); + testStatusHandlerForToEntity(spec.toEntityFlux(String.class)); + testStatusHandlerForToEntity(spec.toEntityFlux(new ParameterizedTypeReference() {})); + } + + private void testStatusHandlerForToEntity(Publisher responsePublisher) { + StepVerifier.create(responsePublisher).expectError(WebClientResponseException.class).verify(); + } + + private ClientRequest verifyAndGetRequest() { ClientRequest request = this.captor.getValue(); verify(this.exchangeFunction).exchange(request); From 204a7fe91f202c364c9ed0a81932c2c873dcf99c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Nov 2020 21:28:18 +0000 Subject: [PATCH 0018/1294] UrlPathHelper.removeJsessionid correctly appends remainder Closes gh-26079 --- .../web/util/UrlPathHelper.java | 2 +- .../web/util/UrlPathHelperTests.java | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index 5c8ff3e4c61d..2fd21b187564 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -635,7 +635,7 @@ private String removeJsessionid(String requestUri) { return requestUri; } String start = requestUri.substring(0, index); - for (int i = key.length(); i < requestUri.length(); i++) { + for (int i = index + key.length(); i < requestUri.length(); i++) { char c = requestUri.charAt(i); if (c == ';' || c == '/') { return start + requestUri.substring(i); diff --git a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java index 695a5ad4fb76..0aea5a0b51d9 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java @@ -128,11 +128,19 @@ public void getRequestRemoveSemicolonContent() { public void getRequestKeepSemicolonContent() { helper.setRemoveSemicolonContent(false); - request.setRequestURI("/foo;a=b;c=d"); - assertThat(helper.getRequestUri(request)).isEqualTo("/foo;a=b;c=d"); - - request.setRequestURI("/foo;jsessionid=c0o7fszeb1"); - assertThat(helper.getRequestUri(request)).isEqualTo("/foo"); + testKeepSemicolonContent("/foo;a=b;c=d", "/foo;a=b;c=d"); + testKeepSemicolonContent("/test;jsessionid=1234", "/test"); + testKeepSemicolonContent("/test;JSESSIONID=1234", "/test"); + testKeepSemicolonContent("/test;jsessionid=1234;a=b", "/test;a=b"); + testKeepSemicolonContent("/test;a=b;jsessionid=1234;c=d", "/test;a=b;c=d"); + testKeepSemicolonContent("/test;jsessionid=1234/anotherTest", "/test/anotherTest"); + testKeepSemicolonContent("/test;jsessionid=;a=b", "/test;a=b"); + testKeepSemicolonContent("/somethingLongerThan12;jsessionid=1234", "/somethingLongerThan12"); + } + + private void testKeepSemicolonContent(String requestUri, String expectedPath) { + request.setRequestURI(requestUri); + assertThat(helper.getRequestUri(request)).isEqualTo(expectedPath); } @Test From b92d249f450920e48e640af6bbd0bd509e7d707d Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 12 Nov 2020 22:00:47 +0000 Subject: [PATCH 0019/1294] AntPathMatcher allows newline in URI template variables Closes gh-23252 --- .../src/main/java/org/springframework/util/AntPathMatcher.java | 2 +- .../test/java/org/springframework/util/AntPathMatcherTests.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java index e1e06b4719de..59087313ea58 100644 --- a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java +++ b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -644,7 +644,7 @@ protected static class AntPathStringMatcher { private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); - private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + private static final String DEFAULT_VARIABLE_PATTERN = "((?s).*)"; private final String rawPattern; diff --git a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java index c85550838daf..8bebf5920ff0 100644 --- a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java +++ b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java @@ -130,6 +130,7 @@ void match() { assertThat(pathMatcher.match("", "")).isTrue(); assertThat(pathMatcher.match("/{bla}.*", "/testing.html")).isTrue(); + assertThat(pathMatcher.match("/{bla}", "//x\ny")).isTrue(); } @Test From 942400ae476550443a041544f5b685096731fd16 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 13 Nov 2020 11:35:34 +0100 Subject: [PATCH 0020/1294] Expose Hibernate Session(Factory)Implementor interface by default Closes gh-26090 --- .../orm/jpa/vendor/HibernateJpaVendorAdapter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java index 9de9c87dec37..8937f66cb3f5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java @@ -25,8 +25,6 @@ import javax.persistence.spi.PersistenceUnitInfo; import javax.persistence.spi.PersistenceUnitTransactionType; -import org.hibernate.Session; -import org.hibernate.SessionFactory; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyTenSevenDialect; @@ -39,6 +37,8 @@ import org.hibernate.dialect.PostgreSQL95Dialect; import org.hibernate.dialect.SQLServer2012Dialect; import org.hibernate.dialect.SybaseDialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import org.springframework.lang.Nullable; @@ -82,8 +82,8 @@ public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter { public HibernateJpaVendorAdapter() { this.persistenceProvider = new SpringHibernateJpaPersistenceProvider(); - this.entityManagerFactoryInterface = SessionFactory.class; - this.entityManagerInterface = Session.class; + this.entityManagerFactoryInterface = SessionFactoryImplementor.class; + this.entityManagerInterface = SessionImplementor.class; } From 500b54b86f6f38808360fd56a17388bd6011c5bf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 13 Nov 2020 17:50:51 +0100 Subject: [PATCH 0021/1294] Expose plain Hibernate Session(Factory) interface by default again See gh-26090 --- .../orm/jpa/vendor/HibernateJpaVendorAdapter.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java index 8937f66cb3f5..ef3324db3523 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java @@ -25,6 +25,8 @@ import javax.persistence.spi.PersistenceUnitInfo; import javax.persistence.spi.PersistenceUnitTransactionType; +import org.hibernate.Session; +import org.hibernate.SessionFactory; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyTenSevenDialect; @@ -37,8 +39,6 @@ import org.hibernate.dialect.PostgreSQL95Dialect; import org.hibernate.dialect.SQLServer2012Dialect; import org.hibernate.dialect.SybaseDialect; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import org.springframework.lang.Nullable; @@ -48,9 +48,9 @@ * EntityManager. Developed and tested against Hibernate 5.3 and 5.4; * backwards-compatible with Hibernate 5.2 at runtime on a best-effort basis. * - *

Exposes Hibernate's persistence provider and EntityManager extension interface, - * and adapts {@link AbstractJpaVendorAdapter}'s common configuration settings. - * Also supports the detection of annotated packages (through + *

Exposes Hibernate's persistence provider and Hibernate's Session as extended + * EntityManager interface, and adapts {@link AbstractJpaVendorAdapter}'s common + * configuration settings. Also supports the detection of annotated packages (through * {@link org.springframework.orm.jpa.persistenceunit.SmartPersistenceUnitInfo#getManagedPackages()}), * e.g. containing Hibernate {@link org.hibernate.annotations.FilterDef} annotations, * along with Spring-driven entity scanning which requires no {@code persistence.xml} @@ -82,8 +82,8 @@ public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter { public HibernateJpaVendorAdapter() { this.persistenceProvider = new SpringHibernateJpaPersistenceProvider(); - this.entityManagerFactoryInterface = SessionFactoryImplementor.class; - this.entityManagerInterface = SessionImplementor.class; + this.entityManagerFactoryInterface = SessionFactory.class; // as of Spring 5.3 + this.entityManagerInterface = Session.class; // as of Spring 5.3 } From 0b580d194d2390c4cb860f337e146b90ba16c6f2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 13 Nov 2020 17:52:51 +0100 Subject: [PATCH 0022/1294] Early log entry for async EntityManagerFactory initialization failure Closes gh-26093 --- .../orm/jpa/AbstractEntityManagerFactoryBean.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java index f417b4d47af8..4a787866287d 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java @@ -418,10 +418,13 @@ private EntityManagerFactory buildNativeEntityManagerFactory() { String message = ex.getMessage(); String causeString = cause.toString(); if (!message.endsWith(causeString)) { - throw new PersistenceException(message + "; nested exception is " + causeString, cause); + ex = new PersistenceException(message + "; nested exception is " + causeString, cause); } } } + if (logger.isErrorEnabled()) { + logger.error("Failed to initialize JPA EntityManagerFactory: " + ex.getMessage()); + } throw ex; } From 0819a9fcc9a1168521995e0bac7de5633a819780 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 15 Nov 2020 20:59:15 +0100 Subject: [PATCH 0023/1294] Discover @DynamicPropertySource methods on enclosing test classes gh-19930 introduced support for finding class-level test configuration annotations on enclosing classes when using JUnit Jupiter @Nested test classes, but support for @DynamicPropertySource methods got overlooked since they are method-level annotations. This commit addresses this shortcoming by introducing full support for @NestedTestConfiguration semantics for @DynamicPropertySource methods on enclosing classes. Closes gh-26091 --- .../test/context/DynamicPropertySource.java | 5 + .../test/context/NestedTestConfiguration.java | 1 + ...micPropertiesContextCustomizerFactory.java | 13 +- .../DynamicPropertySourceNestedTests.java | 185 ++++++++++++++++++ src/docs/asciidoc/testing.adoc | 1 + 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java index 518ce44cb88d..0474f9856bda 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/DynamicPropertySource.java @@ -41,6 +41,11 @@ * is resolved. Typically, method references are used to supply values, as in the * example below. * + *

As of Spring Framework 5.3.2, dynamic properties from methods annotated with + * {@code @DynamicPropertySource} will be inherited from enclosing test + * classes, analogous to inheritance from superclasses and interfaces. See + * {@link NestedTestConfiguration @NestedTestConfiguration} for details. + * *

NOTE: if you use {@code @DynamicPropertySource} in a base * class and discover that tests in subclasses fail because the dynamic properties * change between subclasses, you may need to annotate your base class with diff --git a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java index 48b4b91e9f63..f9d245f82d42 100644 --- a/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/NestedTestConfiguration.java @@ -74,6 +74,7 @@ *

  • {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration}
  • *
  • {@link ActiveProfiles @ActiveProfiles}
  • *
  • {@link TestPropertySource @TestPropertySource}
  • + *
  • {@link DynamicPropertySource @DynamicPropertySource}
  • *
  • {@link org.springframework.test.annotation.DirtiesContext @DirtiesContext}
  • *
  • {@link org.springframework.transaction.annotation.Transactional @Transactional}
  • *
  • {@link org.springframework.test.annotation.Rollback @Rollback}
  • diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java index 7760a43a5067..11779cc12339 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java @@ -17,6 +17,7 @@ package org.springframework.test.context.support; import java.lang.reflect.Method; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -26,12 +27,14 @@ import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestContextAnnotationUtils; /** * {@link ContextCustomizerFactory} to support * {@link DynamicPropertySource @DynamicPropertySource} methods. * * @author Phillip Webb + * @author Sam Brannen * @since 5.2.5 * @see DynamicPropertiesContextCustomizer */ @@ -42,13 +45,21 @@ class DynamicPropertiesContextCustomizerFactory implements ContextCustomizerFact public DynamicPropertiesContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - Set methods = MethodIntrospector.selectMethods(testClass, this::isAnnotated); + Set methods = new LinkedHashSet<>(); + findMethods(testClass, methods); if (methods.isEmpty()) { return null; } return new DynamicPropertiesContextCustomizer(methods); } + private void findMethods(Class testClass, Set methods) { + methods.addAll(MethodIntrospector.selectMethods(testClass, this::isAnnotated)); + if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) { + findMethods(testClass.getEnclosingClass(), methods); + } + } + private boolean isAnnotated(Method method) { return MergedAnnotations.from(method).isPresent(DynamicPropertySource.class); } diff --git a/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java new file mode 100644 index 000000000000..08113cee82cd --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/junit/jupiter/nested/DynamicPropertySourceNestedTests.java @@ -0,0 +1,185 @@ +/* + * 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.test.context.junit.jupiter.nested; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.NestedTestConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration.OVERRIDE; + +/** + * Integration tests that verify support for {@code @Nested} test classes using + * {@link DynamicPropertySource @DynamicPropertySource} in conjunction with the + * {@link SpringExtension} in a JUnit Jupiter environment. + * + * @author Sam Brannen + * @since 5.3.2 + */ +@SpringJUnitConfig +class DynamicPropertySourceNestedTests { + + private static final String TEST_CONTAINER_IP = "DynamicPropertySourceNestedTests.test.container.ip"; + + private static final String TEST_CONTAINER_PORT = "DynamicPropertySourceNestedTests.test.container.port"; + + static DemoContainer container = new DemoContainer(); + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add(TEST_CONTAINER_IP, container::getIpAddress); + registry.add(TEST_CONTAINER_PORT, container::getPort); + } + + + @Test + @DisplayName("@Service has values injected from @DynamicPropertySource") + void serviceHasInjectedValues(@Autowired Service service) { + assertServiceHasInjectedValues(service); + } + + private static void assertServiceHasInjectedValues(Service service) { + assertThat(service.getIp()).isEqualTo("127.0.0.1"); + assertThat(service.getPort()).isEqualTo(4242); + } + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(Config.class) + class DynamicPropertySourceFromSuperclassTests extends DynamicPropertySourceSuperclass { + + @Test + @DisplayName("@Service has values injected from @DynamicPropertySource in superclass") + void serviceHasInjectedValues(@Autowired Service service) { + assertServiceHasInjectedValues(service); + } + } + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(Config.class) + class DynamicPropertySourceFromInterfaceTests implements DynamicPropertySourceInterface { + + @Test + @DisplayName("@Service has values injected from @DynamicPropertySource in interface") + void serviceHasInjectedValues(@Autowired Service service) { + assertServiceHasInjectedValues(service); + } + } + + @Nested + @NestedTestConfiguration(OVERRIDE) + @SpringJUnitConfig(Config.class) + class OverriddenConfigTests { + + @Test + @DisplayName("@Service does not have values injected from @DynamicPropertySource in enclosing class") + void serviceHasDefaultInjectedValues(@Autowired Service service) { + assertThat(service.getIp()).isEqualTo("10.0.0.1"); + assertThat(service.getPort()).isEqualTo(-999); + } + } + + @Nested + class DynamicPropertySourceFromEnclosingClassTests { + + @Test + @DisplayName("@Service has values injected from @DynamicPropertySource in enclosing class") + void serviceHasInjectedValues(@Autowired Service service) { + assertServiceHasInjectedValues(service); + } + + @Nested + class DoubleNestedDynamicPropertySourceFromEnclosingClassTests { + + @Test + @DisplayName("@Service has values injected from @DynamicPropertySource in enclosing class") + void serviceHasInjectedValues(@Autowired Service service) { + assertServiceHasInjectedValues(service); + } + } + } + + + static abstract class DynamicPropertySourceSuperclass { + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add(TEST_CONTAINER_IP, container::getIpAddress); + registry.add(TEST_CONTAINER_PORT, container::getPort); + } + } + + interface DynamicPropertySourceInterface { + + @DynamicPropertySource + static void containerProperties(DynamicPropertyRegistry registry) { + registry.add(TEST_CONTAINER_IP, container::getIpAddress); + registry.add(TEST_CONTAINER_PORT, container::getPort); + } + } + + + @Configuration + @Import(Service.class) + static class Config { + } + + static class Service { + + private final String ip; + + private final int port; + + + Service(@Value("${" + TEST_CONTAINER_IP + ":10.0.0.1}") String ip, @Value("${" + TEST_CONTAINER_PORT + ":-999}") int port) { + this.ip = ip; + this.port = port; + } + + String getIp() { + return this.ip; + } + + int getPort() { + return this.port; + } + } + + static class DemoContainer { + + String getIpAddress() { + return "127.0.0.1"; + } + + int getPort() { + return 4242; + } + } + +} diff --git a/src/docs/asciidoc/testing.adoc b/src/docs/asciidoc/testing.adoc index 9738ec87522b..f4dc2f87e14a 100644 --- a/src/docs/asciidoc/testing.adoc +++ b/src/docs/asciidoc/testing.adoc @@ -1877,6 +1877,7 @@ following annotations. * <> * <> * <> +* <> * <> * <> * <> From 0fd6c100a608d66c150985a906eddbb529ee50bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 16 Nov 2020 14:33:21 +0100 Subject: [PATCH 0024/1294] Polishing See gh-24103 --- .../test/web/servlet/result/ModelResultMatchersDsl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/ModelResultMatchersDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/ModelResultMatchersDsl.kt index 68dc88465aac..a79fd0afbc7e 100644 --- a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/ModelResultMatchersDsl.kt +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/ModelResultMatchersDsl.kt @@ -60,7 +60,7 @@ class ModelResultMatchersDsl internal constructor (private val actions: ResultAc /** * @see ModelResultMatchers.attributeErrorCount */ - fun attributeErrorCount(name: String, expectedCount: Int) { + fun attributeErrorCount(name: String, expectedCount: Int) { actions.andExpect(matchers.attributeErrorCount(name, expectedCount)) } From 83996b12cc0597896722cd4e4c08ed117600f75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=A6=D1=8B=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Mon, 16 Nov 2020 15:51:08 +0200 Subject: [PATCH 0025/1294] Simplify AstUtils.getPropertyAccessorsToTry() --- .../org/springframework/expression/spel/ast/AstUtils.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java index 78bf5536ec3a..ac7a9f0db16d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java @@ -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. @@ -54,10 +54,9 @@ public static List getPropertyAccessorsToTry( } else { if (targetType != null) { - int pos = 0; for (Class clazz : targets) { if (clazz == targetType) { // put exact matches on the front to be tried first? - specificAccessors.add(pos++, resolver); + specificAccessors.add(resolver); } else if (clazz.isAssignableFrom(targetType)) { // put supertype matches at the end of the // specificAccessor list From 7206a23d33132a0c0fa1ef6ad00cbf980165f4d9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Nov 2020 17:40:39 +0100 Subject: [PATCH 0026/1294] Consistent attribute value spelling for PATH_ATTRIBUTE See gh-24945 --- .../org/springframework/web/util/ServletRequestPathUtils.java | 2 +- .../main/java/org/springframework/web/util/UrlPathHelper.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java index 31722320aacc..200478ea55e2 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java @@ -39,7 +39,7 @@ public abstract class ServletRequestPathUtils { /** Name of Servlet request attribute that holds the parsed {@link RequestPath}. */ - public static final String PATH_ATTRIBUTE = ServletRequestPathUtils.class.getName() + ".path"; + public static final String PATH_ATTRIBUTE = ServletRequestPathUtils.class.getName() + ".PATH"; /** diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index 2fd21b187564..8c23248ddfa3 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -58,9 +58,9 @@ public class UrlPathHelper { * {@link #getLookupPathForRequest resolved} lookupPath. * @since 5.3 */ - public static final String PATH_ATTRIBUTE = UrlPathHelper.class.getName() + ".path"; + public static final String PATH_ATTRIBUTE = UrlPathHelper.class.getName() + ".PATH"; - private static boolean isServlet4Present = + private static final boolean isServlet4Present = ClassUtils.isPresent("javax.servlet.http.HttpServletMapping", UrlPathHelper.class.getClassLoader()); From 51b1306d7051164716e69c78df01ba330ce0e211 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Nov 2020 17:41:09 +0100 Subject: [PATCH 0027/1294] Move coroutines invocation decision to invokeWithinTransaction See gh-26092 --- .../interceptor/TransactionAspectSupport.java | 44 ++++++++++++++++--- .../interceptor/TransactionInterceptor.java | 31 +++++++------ 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java index 1ecef7f73332..18088595ea7d 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java @@ -34,6 +34,7 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; +import org.springframework.core.CoroutinesUtils; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.NamedThreadLocal; @@ -342,9 +343,16 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class targe if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) { boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method); - boolean hasSuspendingFlowReturnType = isSuspendingFunction && COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()); + boolean hasSuspendingFlowReturnType = isSuspendingFunction && + COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()); + if (isSuspendingFunction && !(invocation instanceof CoroutinesInvocationCallback)) { + throw new IllegalStateException("Coroutines invocation not supported: " + method); + } + CoroutinesInvocationCallback corInv = (isSuspendingFunction ? (CoroutinesInvocationCallback) invocation : null); + ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> { - Class reactiveType = (isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType()); + Class reactiveType = + (isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType()); ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType); if (adapter == null) { throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " + @@ -352,9 +360,18 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class targe } return new ReactiveTransactionSupport(adapter); }); - Object result = txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm); - return (isSuspendingFunction ? (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow((Publisher) result) : - KotlinDelegate.awaitSingleOrNull((Publisher) result, ((CoroutinesInvocationCallback) invocation).getContinuation())) : result); + + InvocationCallback callback = invocation; + if (corInv != null) { + callback = () -> CoroutinesUtils.invokeSuspendingFunction(method, corInv.getTarget(), corInv.getArguments()); + } + Object result = txSupport.invokeWithinTransaction(method, targetClass, callback, txAttr, (ReactiveTransactionManager) tm); + if (corInv != null) { + Publisher pr = (Publisher) result; + return (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow(pr) : + KotlinDelegate.awaitSingleOrNull(pr, corInv.getContinuation())); + } + return result; } PlatformTransactionManager ptm = asPlatformTransactionManager(tm); @@ -789,9 +806,20 @@ protected interface InvocationCallback { Object proceedWithInvocation() throws Throwable; } + + /** + * Coroutines-supporting extension of the callback interface. + */ protected interface CoroutinesInvocationCallback extends InvocationCallback { - Object getContinuation(); + Object getTarget(); + + Object[] getArguments(); + + default Object getContinuation() { + Object[] args = getArguments(); + return args[args.length - 1]; + } } @@ -876,7 +904,9 @@ public Object invokeWithinTransaction(Method method, @Nullable Class targetCl String joinpointIdentification = methodIdentification(method, targetClass, txAttr); // For Mono and suspending functions not returning kotlinx.coroutines.flow.Flow - if (Mono.class.isAssignableFrom(method.getReturnType()) || (KotlinDetector.isSuspendingFunction(method) && !COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()))) { + if (Mono.class.isAssignableFrom(method.getReturnType()) || (KotlinDetector.isSuspendingFunction(method) && + !COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()))) { + return TransactionContextManager.currentContext().flatMap(context -> createTransactionIfNecessary(rtm, txAttr, joinpointIdentification).flatMap(it -> { try { diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionInterceptor.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionInterceptor.java index a6373d2a05f5..788c1f251994 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionInterceptor.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionInterceptor.java @@ -27,8 +27,6 @@ import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanFactory; -import org.springframework.core.CoroutinesUtils; -import org.springframework.core.KotlinDetector; import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionManager; @@ -118,20 +116,21 @@ public Object invoke(MethodInvocation invocation) throws Throwable { Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Adapt to TransactionAspectSupport's invokeWithinTransaction... - if (KotlinDetector.isSuspendingFunction(invocation.getMethod())) { - InvocationCallback callback = new CoroutinesInvocationCallback() { - @Override - public Object proceedWithInvocation() { - return CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(), invocation.getArguments()); - } - @Override - public Object getContinuation() { - return invocation.getArguments()[invocation.getArguments().length - 1]; - } - }; - return invokeWithinTransaction(invocation.getMethod(), targetClass, callback); - } - return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); + return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() { + @Override + @Nullable + public Object proceedWithInvocation() throws Throwable { + return invocation.proceed(); + } + @Override + public Object getTarget() { + return invocation.getThis(); + } + @Override + public Object[] getArguments() { + return invocation.getArguments(); + } + }); } From 238354a081e6fdfe3af370d566bd750263b6e274 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Nov 2020 17:42:22 +0100 Subject: [PATCH 0028/1294] Polishing --- .../transaction/aspectj/AbstractTransactionAspect.aj | 5 +++-- .../main/kotlin/org/springframework/core/CoroutinesUtils.kt | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj index aed8e4ab65a8..782ca35e0777 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/AbstractTransactionAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 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. @@ -59,7 +59,8 @@ public abstract aspect AbstractTransactionAspect extends TransactionAspectSuppor @Override public void destroy() { - clearTransactionManagerCache(); // An aspect is basically a singleton + // An aspect is basically a singleton -> cleanup on destruction + clearTransactionManagerCache(); } @SuppressAjWarnings("adviceDidNotMatch") diff --git a/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt b/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt index cfde83920127..e62281d8edb8 100644 --- a/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt +++ b/spring-core/kotlin-coroutines/src/main/kotlin/org/springframework/core/CoroutinesUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -58,10 +58,10 @@ internal fun monoToDeferred(source: Mono) = * @since 5.2 */ @Suppress("UNCHECKED_CAST") -fun invokeSuspendingFunction(method: Method, bean: Any, vararg args: Any?): Publisher<*> { +fun invokeSuspendingFunction(method: Method, target: Any, vararg args: Any?): Publisher<*> { val function = method.kotlinFunction!! val mono = mono(Dispatchers.Unconfined) { - function.callSuspend(bean, *args.sliceArray(0..(args.size-2))).let { if (it == Unit) null else it } + function.callSuspend(target, *args.sliceArray(0..(args.size-2))).let { if (it == Unit) null else it } }.onErrorMap(InvocationTargetException::class.java) { it.targetException } return if (function.returnType.classifier == Flow::class) { mono.flatMapMany { (it as Flow).asFlux() } From c22a483c3d93d55df36a9c00f177faa86dcef431 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 16 Nov 2020 17:01:35 +0000 Subject: [PATCH 0029/1294] Update section on type conversion for web method arguments Closes gh-26088 --- src/docs/asciidoc/web/webflux.adoc | 6 ++++++ src/docs/asciidoc/web/webmvc.adoc | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index c3e0d2cf3462..7e1e52d3c422 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -2009,6 +2009,12 @@ By default, simple types (such as `int`, `long`, `Date`, and others) are support can be customized through a `WebDataBinder` (see <>) or by registering `Formatters` with the `FormattingConversionService` (see <>). +A practical issue in type conversion is the treatment of an empty String source value. +Such a value is treated as missing if it becomes `null` as a result of type conversion. +This can be the case for `Long`, `UUID`, and other target types. If you want to allow `null` +to be injected, either use the `required` flag on the argument annotation, or declare the +argument as `@Nullable`. + [[webflux-ann-matrix-variables]] ==== Matrix Variables diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 16143be5917d..180de3e7f9a6 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -2216,6 +2216,13 @@ type conversion through a `WebDataBinder` (see <>) or by reg `Formatters` with the `FormattingConversionService`. See <>. +A practical issue in type conversion is the treatment of an empty String source value. +Such a value is treated as missing if it becomes `null` as a result of type conversion. +This can be the case for `Long`, `UUID`, and other target types. If you want to allow `null` +to be injected, either use the `required` flag on the argument annotation, or declare the +argument as `@Nullable`. + + [[mvc-ann-matrix-variables]] ==== Matrix Variables From 3d31750acc14e1172fe83075bd1bb08b3e6db0e9 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 16 Nov 2020 18:20:19 +0100 Subject: [PATCH 0030/1294] Don't swallow logs during promotion job in CI Prior to this commit, the promote task in the CI release pipeline would write the "concourse-release-scripts" CONSOLE logs to /dev/null; this commit ensures that we can read those while promoting artifacts. --- ci/scripts/promote-version.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/scripts/promote-version.sh b/ci/scripts/promote-version.sh index 020db4f300b3..3b8dab0151db 100755 --- a/ci/scripts/promote-version.sh +++ b/ci/scripts/promote-version.sh @@ -6,11 +6,11 @@ CONFIG_DIR=git-repo/ci/config version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json -java -jar /opt/concourse-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } +java -jar /opt/concourse-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } java -jar /opt/concourse-release-scripts.jar \ --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - distribute $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } + distribute $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } echo "Promotion complete" echo $version > version/version From bc5a10c70d48b69395eb121cbbce13ac2bd05ffb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Nov 2020 19:51:50 +0100 Subject: [PATCH 0031/1294] Document that Hibernate Search 5.11.6 is required for Spring JPA compatibility Closes gh-26090 --- src/docs/asciidoc/data-access.adoc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/docs/asciidoc/data-access.adoc b/src/docs/asciidoc/data-access.adoc index ed552dc63dc2..a2ca75459c30 100644 --- a/src/docs/asciidoc/data-access.adoc +++ b/src/docs/asciidoc/data-access.adoc @@ -7419,16 +7419,17 @@ exception hierarchies. [[orm-hibernate]] === Hibernate -We start with a coverage of https://hibernate.org/[Hibernate 5] in a Spring -environment, using it to demonstrate the approach that Spring takes towards integrating -OR mappers. This section covers many issues in detail and shows different variations -of DAO implementations and transaction demarcation. Most of these patterns can be -directly translated to all other supported ORM tools. The later sections in this -chapter then cover the other ORM technologies and show brief examples. +We start with a coverage of https://hibernate.org/[Hibernate 5] in a Spring environment, +using it to demonstrate the approach that Spring takes towards integrating OR mappers. +This section covers many issues in detail and shows different variations of DAO +implementations and transaction demarcation. Most of these patterns can be directly +translated to all other supported ORM tools. The later sections in this chapter then +cover the other ORM technologies and show brief examples. NOTE: As of Spring Framework 5.3, Spring requires Hibernate ORM 5.2+ for Spring's `HibernateJpaVendorAdapter` as well as for a native Hibernate `SessionFactory` setup. Is is strongly recommended to go with Hibernate ORM 5.4 for a newly started application. +For use with `HibernateJpaVendorAdapter`, Hibernate Search needs to be upgraded to 5.11.6. [[orm-session-factory-setup]] From 041bff4e56fc60f9c1bf7af8def87c1d663d4d26 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 16 Nov 2020 20:36:29 +0100 Subject: [PATCH 0032/1294] Upgrade to Log4J 2.14, Reactor 2020.0.1, Netty 4.1.54, Protobuf 3.14, XStream 1.4.14, OpenPDF 1.3.23, AssertJ 3.18.1, MockK 1.10.2, HtmlUnit 2.45, Checkstyle 8.37 --- build.gradle | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 49e9569a6189..d42039382e4b 100644 --- a/build.gradle +++ b/build.gradle @@ -26,8 +26,8 @@ configure(allprojects) { project -> dependencyManagement { imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.11.3" - mavenBom "io.netty:netty-bom:4.1.53.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.0" + mavenBom "io.netty:netty-bom:4.1.54.Final" + mavenBom "io.projectreactor:reactor-bom:2020.0.1" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" mavenBom "io.rsocket:rsocket-bom:1.1.0" mavenBom "org.eclipse.jetty:jetty-bom:9.4.34.v20201102" @@ -36,7 +36,7 @@ configure(allprojects) { project -> mavenBom "org.junit:junit-bom:5.7.0" } dependencies { - dependencySet(group: 'org.apache.logging.log4j', version: '2.13.3') { + dependencySet(group: 'org.apache.logging.log4j', version: '2.14.0') { entry 'log4j-api' entry 'log4j-core' entry 'log4j-jul' @@ -73,9 +73,9 @@ configure(allprojects) { project -> exclude group: "stax", name: "stax-api" } dependency "com.google.code.gson:gson:2.8.6" - dependency "com.google.protobuf:protobuf-java-util:3.13.0" + dependency "com.google.protobuf:protobuf-java-util:3.14.0" dependency "com.googlecode.protobuf-java-format:protobuf-java-format:1.4" - dependency("com.thoughtworks.xstream:xstream:1.4.13") { + dependency("com.thoughtworks.xstream:xstream:1.4.14") { exclude group: "xpp3", name: "xpp3_min" exclude group: "xmlpull", name: "xmlpull" } @@ -96,7 +96,7 @@ configure(allprojects) { project -> dependency "com.h2database:h2:1.4.200" dependency "com.github.ben-manes.caffeine:caffeine:2.8.6" - dependency "com.github.librepdf:openpdf:1.3.22" + dependency "com.github.librepdf:openpdf:1.3.23" dependency "com.rometools:rome:1.15.0" dependency "commons-io:commons-io:2.5" dependency "io.vavr:vavr:0.10.3" @@ -190,7 +190,7 @@ configure(allprojects) { project -> dependency "org.testng:testng:7.3.0" dependency "org.hamcrest:hamcrest:2.1" dependency "org.awaitility:awaitility:3.1.6" - dependency "org.assertj:assertj-core:3.18.0" + dependency "org.assertj:assertj-core:3.18.1" dependencySet(group: 'org.xmlunit', version: '2.6.2') { entry 'xmlunit-assertj' entry('xmlunit-matchers') { @@ -203,12 +203,12 @@ configure(allprojects) { project -> } entry 'mockito-junit-jupiter' } - dependency "io.mockk:mockk:1.10.0" + dependency "io.mockk:mockk:1.10.2" - dependency("net.sourceforge.htmlunit:htmlunit:2.44.0") { + dependency("net.sourceforge.htmlunit:htmlunit:2.45.0") { exclude group: "commons-logging", name: "commons-logging" } - dependency("org.seleniumhq.selenium:htmlunit-driver:2.44.0") { + dependency("org.seleniumhq.selenium:htmlunit-driver:2.45.0") { exclude group: "commons-logging", name: "commons-logging" } dependency("org.seleniumhq.selenium:selenium-java:3.141.59") { @@ -339,7 +339,7 @@ configure([rootProject] + javaProjects) { project -> } checkstyle { - toolVersion = "8.36.2" + toolVersion = "8.37" configDirectory.set(rootProject.file("src/checkstyle")) } From 1b1ba479125ecd1051d14742d89f492376176130 Mon Sep 17 00:00:00 2001 From: izeye Date: Tue, 17 Nov 2020 12:59:42 +0900 Subject: [PATCH 0033/1294] Avoid char array creation in AbstractAspectJAdvice.isVariableName() See gh-26100 --- .../springframework/aop/aspectj/AbstractAspectJAdvice.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index 1eb0e274562e..7515334928f3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -354,8 +354,8 @@ private static boolean isVariableName(String name) { if (!Character.isJavaIdentifierStart(name.charAt(0))) { return false; } - for (char ch: name.toCharArray()) { - if (!Character.isJavaIdentifierPart(ch)) { + for (int i = 1; i < name.length(); i++) { + if (!Character.isJavaIdentifierPart(name.charAt(i))) { return false; } } From 9139cb85bd23d93fe72571d5cc23431860449f7e Mon Sep 17 00:00:00 2001 From: thanus Date: Sat, 3 Oct 2020 18:50:09 +0200 Subject: [PATCH 0034/1294] Update javax.mail reference to jakarta.mail --- src/docs/asciidoc/integration.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index 1115328d8e1a..bfd049518c60 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -4421,10 +4421,10 @@ This section describes how to send email with the Spring Framework. The following JAR needs to be on the classpath of your application in order to use the Spring Framework's email library: -* The https://javaee.github.io/javamail/[JavaMail] library +* The https://eclipse-ee4j.github.io/mail/[JavaMail] library This library is freely available on the web -- for example, in Maven Central as -`com.sun.mail:javax.mail`. +`com.sun.mail:jakarta.mail`. **** The Spring Framework provides a helpful utility library for sending email that shields From c9b27af64f41cc4fd06ee41a0faaca867e3cba4e Mon Sep 17 00:00:00 2001 From: Marten Deinum Date: Tue, 17 Nov 2020 11:52:58 +0100 Subject: [PATCH 0035/1294] Reduce overhead of char[] creation There are more locations which could benefit from not using a toCharArray on a String, but rather use the charAt method from the String itself. This to prevent an additional copy of the char[] being created. --- .../aop/aspectj/AspectJAdviceParameterNameDiscoverer.java | 4 ++-- .../main/java/org/springframework/core/Conventions.java | 8 ++++---- .../java/org/springframework/http/ContentDisposition.java | 3 ++- .../java/org/springframework/http/ResponseCookie.java | 3 +-- .../springframework/http/server/DefaultPathContainer.java | 6 +----- .../web/util/HierarchicalUriComponents.java | 3 ++- .../java/org/springframework/web/util/UriComponents.java | 3 ++- .../web/util/pattern/LiteralPathElement.java | 7 +++---- .../web/util/pattern/SingleCharWildcardedPathElement.java | 7 +++---- 9 files changed, 20 insertions(+), 24 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java index ec58afb29652..be072a1a583d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java @@ -475,8 +475,8 @@ private String maybeExtractVariableName(@Nullable String candidateToken) { } if (Character.isJavaIdentifierStart(candidateToken.charAt(0)) && Character.isLowerCase(candidateToken.charAt(0))) { - char[] tokenChars = candidateToken.toCharArray(); - for (char tokenChar : tokenChars) { + for (int i = 1; i < candidateToken.length(); i++) { + char tokenChar = candidateToken.charAt(i); if (!Character.isJavaIdentifierPart(tokenChar)) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/Conventions.java b/spring-core/src/main/java/org/springframework/core/Conventions.java index 691f1811a8de..70daea7b454e 100644 --- a/spring-core/src/main/java/org/springframework/core/Conventions.java +++ b/spring-core/src/main/java/org/springframework/core/Conventions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -225,11 +225,11 @@ public static String attributeNameToPropertyName(String attributeName) { if (!attributeName.contains("-")) { return attributeName; } - char[] chars = attributeName.toCharArray(); - char[] result = new char[chars.length -1]; // not completely accurate but good guess + char[] result = new char[attributeName.length() -1]; // not completely accurate but good guess int currPos = 0; boolean upperCaseNext = false; - for (char c : chars) { + for (int i = 0; i < attributeName.length(); i++ ) { + char c = attributeName.charAt(i); if (c == '-') { upperCaseNext = true; } diff --git a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java index 8b735a22eeb8..ce75abdc096d 100644 --- a/spring-web/src/main/java/org/springframework/http/ContentDisposition.java +++ b/spring-web/src/main/java/org/springframework/http/ContentDisposition.java @@ -485,7 +485,8 @@ private static String escapeQuotationsInFilename(String filename) { } boolean escaped = false; StringBuilder sb = new StringBuilder(); - for (char c : filename.toCharArray()) { + for (int i = 0; i < filename.length() ; i++) { + char c = filename.charAt(i); if (!escaped && c == '"') { sb.append("\\\""); } diff --git a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java index 05ea8d372fcb..01048cab83c4 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java @@ -386,9 +386,8 @@ public static void validateCookieValue(@Nullable String value) { start = 1; end--; } - char[] chars = value.toCharArray(); for (int i = start; i < end; i++) { - char c = chars[i]; + char c = value.charAt(i); if (c < 0x21 || c == 0x22 || c == 0x2c || c == 0x3b || c == 0x5c || c == 0x7f) { throw new IllegalArgumentException( "RFC2616 cookie value cannot have '" + c + "'"); diff --git a/spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java b/spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java index 4476df73914a..ddea35fc75c5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java +++ b/spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java @@ -232,8 +232,6 @@ private static class DefaultPathSegment implements PathSegment { private final String valueToMatch; - private final char[] valueToMatchAsChars; - private final MultiValueMap parameters; @@ -243,7 +241,6 @@ private static class DefaultPathSegment implements PathSegment { DefaultPathSegment(String value, String valueToMatch, MultiValueMap params) { this.value = value; this.valueToMatch = valueToMatch; - this.valueToMatchAsChars = valueToMatch.toCharArray(); this.parameters = CollectionUtils.unmodifiableMultiValueMap(params); } @@ -254,7 +251,6 @@ private static class DefaultPathSegment implements PathSegment { this.value = value; this.valueToMatch = value.contains(separator.encodedSequence()) ? value.replaceAll(separator.encodedSequence(), separator.value()) : value; - this.valueToMatchAsChars = this.valueToMatch.toCharArray(); this.parameters = EMPTY_PARAMS; } @@ -271,7 +267,7 @@ public String valueToMatch() { @Override public char[] valueToMatchAsChars() { - return this.valueToMatchAsChars; + return this.valueToMatch.toCharArray(); } @Override diff --git a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java index 6a28854714f2..37633018a9d9 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/HierarchicalUriComponents.java @@ -791,7 +791,8 @@ public String apply(String source, Type type) { clear(this.currentLiteral); clear(this.currentVariable); clear(this.output); - for (char c : source.toCharArray()) { + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); if (c == '{') { level++; if (level == 1) { diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java index b197cd9e55f0..ea047e631a8e 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponents.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponents.java @@ -278,7 +278,8 @@ private static String sanitizeSource(String source) { int level = 0; int lastCharIndex = 0; char[] chars = new char[source.length()]; - for (char c : source.toCharArray()) { + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); if (c == '{') { level++; } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java index 715831f70046..224d5d721708 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java @@ -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. @@ -69,10 +69,9 @@ public boolean matches(int pathIndex, MatchingContext matchingContext) { return false; } - char[] data = ((PathContainer.PathSegment)element).valueToMatchAsChars(); if (this.caseSensitive) { for (int i = 0; i < this.len; i++) { - if (data[i] != this.text[i]) { + if (value.charAt(i) != this.text[i]) { return false; } } @@ -80,7 +79,7 @@ public boolean matches(int pathIndex, MatchingContext matchingContext) { else { for (int i = 0; i < this.len; i++) { // TODO revisit performance if doing a lot of case insensitive matching - if (Character.toLowerCase(data[i]) != this.text[i]) { + if (Character.toLowerCase(value.charAt(i)) != this.text[i]) { return false; } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java index 717a7c4a9782..7aa748af6f47 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java @@ -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. @@ -74,11 +74,10 @@ public boolean matches(int pathIndex, MatchingContext matchingContext) { return false; } - char[] data = ((PathSegment)element).valueToMatchAsChars(); if (this.caseSensitive) { for (int i = 0; i < this.len; i++) { char ch = this.text[i]; - if ((ch != '?') && (ch != data[i])) { + if ((ch != '?') && (ch != value.charAt((i)))) { return false; } } @@ -87,7 +86,7 @@ public boolean matches(int pathIndex, MatchingContext matchingContext) { for (int i = 0; i < this.len; i++) { char ch = this.text[i]; // TODO revisit performance if doing a lot of case insensitive matching - if ((ch != '?') && (ch != Character.toLowerCase(data[i]))) { + if ((ch != '?') && (ch != Character.toLowerCase(value.charAt(i)))) { return false; } } From b56dbd2aa8b5c521be85c799f934b70bac545fb2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 17 Nov 2020 14:42:41 +0100 Subject: [PATCH 0036/1294] Explicitly mention Jakarta Mail 1.6 next to JavaMail See gh-25855 --- src/docs/asciidoc/integration.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index bfd049518c60..59afa46ece52 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -4421,10 +4421,11 @@ This section describes how to send email with the Spring Framework. The following JAR needs to be on the classpath of your application in order to use the Spring Framework's email library: -* The https://eclipse-ee4j.github.io/mail/[JavaMail] library +* The https://eclipse-ee4j.github.io/mail/[JavaMail / Jakarta Mail 1.6] library This library is freely available on the web -- for example, in Maven Central as -`com.sun.mail:jakarta.mail`. +`com.sun.mail:jakarta.mail`. Please make sure to use the latest 1.6.x version +rather than Jakarta Mail 2.0 (which comes with a different package namespace). **** The Spring Framework provides a helpful utility library for sending email that shields From 2ee231dee24a90b36041f898fb544af07f66ee48 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 17 Nov 2020 14:43:17 +0100 Subject: [PATCH 0037/1294] Document that @Transactional does not propagate to new threads Closes gh-25439 --- .../transaction/annotation/Transactional.java | 19 +++-- .../RuleBasedTransactionAttribute.java | 2 +- src/docs/asciidoc/data-access.adoc | 73 +++++++++++-------- 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java index 4af6be767539..caad238aae12 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java @@ -38,16 +38,25 @@ * {@link org.springframework.transaction.interceptor.RuleBasedTransactionAttribute} * class, and in fact {@link AnnotationTransactionAttributeSource} will directly * convert the data to the latter class, so that Spring's transaction support code - * does not have to know about annotations. If no rules are relevant to the exception, - * it will be treated like - * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute} - * (rolling back on {@link RuntimeException} and {@link Error} but not on checked - * exceptions). + * does not have to know about annotations. If no custom rollback rules apply, + * the transaction will roll back on {@link RuntimeException} and {@link Error} + * but not on checked exceptions. * *

    For specific information about the semantics of this annotation's attributes, * consult the {@link org.springframework.transaction.TransactionDefinition} and * {@link org.springframework.transaction.interceptor.TransactionAttribute} javadocs. * + *

    This annotation commonly works with thread-bound transactions managed by + * {@link org.springframework.transaction.PlatformTransactionManager}, exposing a + * transaction to all data access operations within the current execution thread. + * Note: This does NOT propagate to newly started threads within the method. + * + *

    Alternatively, this annotation may demarcate a reactive transaction managed + * by {@link org.springframework.transaction.ReactiveTransactionManager} which + * uses the Reactor context instead of thread-local attributes. As a consequence, + * all participating data access operations need to execute within the same + * Reactor context in the same reactive pipeline. + * * @author Colin Sampaleanu * @author Juergen Hoeller * @author Sam Brannen diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java index ecb231abec52..0451f0518ecb 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java @@ -28,7 +28,7 @@ /** * TransactionAttribute implementation that works out whether a given exception * should cause transaction rollback by applying a number of rollback rules, - * both positive and negative. If no rules are relevant to the exception, it + * both positive and negative. If no custom rollback rules apply, this attribute * behaves like DefaultTransactionAttribute (rolling back on runtime exceptions). * *

    {@link TransactionAttributeEditor} creates objects of this class. diff --git a/src/docs/asciidoc/data-access.adoc b/src/docs/asciidoc/data-access.adoc index a2ca75459c30..0fa295f6b67d 100644 --- a/src/docs/asciidoc/data-access.adoc +++ b/src/docs/asciidoc/data-access.adoc @@ -566,7 +566,7 @@ abstractions mentioned earlier. [[transaction-declarative]] -=== Declarative transaction management +=== Declarative Transaction Management NOTE: Most Spring Framework users choose declarative transaction management. This option has the least impact on application code and, hence, is most consistent with the ideals of a @@ -637,7 +637,7 @@ around method invocations. NOTE: Spring AOP is covered in <>. -Spring Frameworks's `TransactionInterceptor` provides transaction management for +Spring Framework's `TransactionInterceptor` provides transaction management for imperative and reactive programming models. The interceptor detects the desired flavor of transaction management by inspecting the method return type. Methods returning a reactive type such as `Publisher` or Kotlin `Flow` (or a subtype of those) qualify for reactive @@ -648,6 +648,18 @@ Transaction management flavors impact which transaction manager is required. Imp transactions require a `PlatformTransactionManager`, while reactive transactions use `ReactiveTransactionManager` implementations. +[NOTE] +==== +`@Transactional` commonly works with thread-bound transactions managed by +`PlatformTransactionManager`, exposing a transaction to all data access operations within +the current execution thread. Note: This does _not_ propagate to newly started threads +within the method. + +A reactive transaction managed by `ReactiveTransactionManager` uses the Reactor context +instead of thread-local attributes. As a consequence, all participating data access +operations need to execute within the same Reactor context in the same reactive pipeline. +==== + The following image shows a conceptual view of calling a method on a transactional proxy: image::images/tx.png[] @@ -2844,30 +2856,29 @@ specific to each technology. Spring provides a convenient translation from technology-specific exceptions, such as `SQLException` to its own exception class hierarchy, which has `DataAccessException` as -the root exception. These exceptions wrap the original exception so that there is never any -risk that you might lose any information about what might have gone wrong. +the root exception. These exceptions wrap the original exception so that there is never +any risk that you might lose any information about what might have gone wrong. In addition to JDBC exceptions, Spring can also wrap JPA- and Hibernate-specific exceptions, -converting them to a set of focused runtime exceptions. -This lets you handle most non-recoverable persistence exceptions -in only the appropriate layers, without having annoying boilerplate -catch-and-throw blocks and exception declarations in your DAOs. (You can still trap -and handle exceptions anywhere you need to though.) As mentioned above, JDBC -exceptions (including database-specific dialects) are also converted to the same +converting them to a set of focused runtime exceptions. This lets you handle most +non-recoverable persistence exceptions in only the appropriate layers, without having +annoying boilerplate catch-and-throw blocks and exception declarations in your DAOs. +(You can still trap and handle exceptions anywhere you need to though.) As mentioned above, +JDBC exceptions (including database-specific dialects) are also converted to the same hierarchy, meaning that you can perform some operations with JDBC within a consistent programming model. -The preceding discussion holds true for the various template classes in Spring's support for various ORM -frameworks. If you use the interceptor-based classes, the application must care -about handling `HibernateExceptions` and `PersistenceExceptions` itself, preferably by -delegating to the `convertHibernateAccessException(..)` or -`convertJpaAccessException()` methods, respectively, of `SessionFactoryUtils`. These methods convert the exceptions +The preceding discussion holds true for the various template classes in Spring's support +for various ORM frameworks. If you use the interceptor-based classes, the application must +care about handling `HibernateExceptions` and `PersistenceExceptions` itself, preferably by +delegating to the `convertHibernateAccessException(..)` or `convertJpaAccessException(..)` +methods, respectively, of `SessionFactoryUtils`. These methods convert the exceptions to exceptions that are compatible with the exceptions in the `org.springframework.dao` -exception hierarchy. As `PersistenceExceptions` are unchecked, they can get -thrown, too (sacrificing generic DAO abstraction in terms of exceptions, though). +exception hierarchy. As `PersistenceExceptions` are unchecked, they can get thrown, too +(sacrificing generic DAO abstraction in terms of exceptions, though). -The following image shows the exception hierarchy that Spring provides. (Note that the -class hierarchy detailed in the image shows only a subset of the entire +The following image shows the exception hierarchy that Spring provides. +(Note that the class hierarchy detailed in the image shows only a subset of the entire `DataAccessException` hierarchy.) image::images/DataAccessException.png[] @@ -4232,14 +4243,14 @@ interface that wraps a single `Connection` that is not closed after each use. This is not multi-threading capable. If any client code calls `close` on the assumption of a pooled connection (as when using -persistence tools), you should set the `suppressClose` property to `true`. This setting returns a -close-suppressing proxy that wraps the physical connection. Note that you can no longer -cast this to a native Oracle `Connection` or a similar object. +persistence tools), you should set the `suppressClose` property to `true`. This setting +returns a close-suppressing proxy that wraps the physical connection. Note that you can +no longer cast this to a native Oracle `Connection` or a similar object. -`SingleConnectionDataSource` is primarily a test class. For example, it enables easy testing of code outside an -application server, in conjunction with a simple JNDI environment. In contrast to -`DriverManagerDataSource`, it reuses the same connection all the time, avoiding -excessive creation of physical connections. +`SingleConnectionDataSource` is primarily a test class. It typically enables easy testing +of code outside an application server, in conjunction with a simple JNDI environment. +In contrast to `DriverManagerDataSource`, it reuses the same connection all the time, +avoiding excessive creation of physical connections. @@ -8810,8 +8821,8 @@ can do so by using the following `applicationContext.xml`: ---- This application context uses XStream, but we could have used any of the other marshaller -instances described later in this chapter. Note that, by default, XStream does not require any further -configuration, so the bean definition is rather simple. Also note that the +instances described later in this chapter. Note that, by default, XStream does not require +any further configuration, so the bean definition is rather simple. Also note that the `XStreamMarshaller` implements both `Marshaller` and `Unmarshaller`, so we can refer to the `xstreamMarshaller` bean in both the `marshaller` and `unmarshaller` property of the application. @@ -8829,8 +8840,8 @@ This sample application produces the following `settings.xml` file: [[oxm-schema-based-config]] === XML Configuration Namespace -You can configure marshallers more concisely by using tags from the OXM namespace. To -make these tags available, you must first reference the appropriate schema in the +You can configure marshallers more concisely by using tags from the OXM namespace. +To make these tags available, you must first reference the appropriate schema in the preamble of the XML configuration file. The following example shows how to do so: [source,xml,indent=0] @@ -9073,7 +9084,7 @@ vulnerabilities do not get invoked. NOTE: Note that XStream is an XML serialization library, not a data binding library. Therefore, it has limited namespace support. As a result, it is rather unsuitable for usage -within Web services. +within Web Services. From b29723623b33f1ce8fbffec19e4c4dec91bf2da7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 17 Nov 2020 14:45:45 +0100 Subject: [PATCH 0038/1294] Encode hash symbol in jar file path (for compatibility with JDK 11+) Closes gh-26104 --- .../core/io/support/PathMatchingResourcePatternResolver.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java index 39d79fb2af5d..69cd66134883 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java @@ -432,6 +432,9 @@ protected void addClassPathManifestEntries(Set result) { // Possibly "c:" drive prefix on Windows, to be upper-cased for proper duplicate detection filePath = StringUtils.capitalize(filePath); } + // # can appear in directories/filenames, java.net.URL should not treat it as a fragment + filePath = StringUtils.replace(filePath, "#", "%23"); + // Build URL that points to the root of the jar file UrlResource jarResource = new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + filePath + ResourceUtils.JAR_URL_SEPARATOR); // Potentially overlapping with URLClassLoader.getURLs() result above! From de0b5bc5a1916737b116ae5c645898a08ac4f07e Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 17 Nov 2020 15:21:10 +0100 Subject: [PATCH 0039/1294] Polish Kotlin code snippet in reference docs --- src/docs/asciidoc/core/core-beans.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index df0f1f33f29d..ba79c2974afc 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -11064,13 +11064,13 @@ Here is an example of instrumentation in the `AnnotationConfigApplicationContext .Kotlin ---- // create a startup step and start recording - val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan"); + val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan") // add tagging information to the current step - scanPackages.tag("packages", () -> Arrays.toString(basePackages)); + scanPackages.tag("packages", () -> Arrays.toString(basePackages)) // perform the actual phase we're instrumenting - this.scanner.scan(basePackages); + this.scanner.scan(basePackages) // end the current step - scanPackages.end(); + scanPackages.end() ---- The application context is already instrumented with multiple steps. From 41835ba5a4d66c23347bc40859598e9bb14d40ca Mon Sep 17 00:00:00 2001 From: Marten Deinum Date: Tue, 17 Nov 2020 15:04:33 +0100 Subject: [PATCH 0040/1294] Re-use the isVariableName method Prior to this change the checks for isVariableName were duplicated in 2 different locations. The logic has been moved to AspectJProxyUtils to allow for re-use in those places and so that it benefits from any optimizations that are done. --- .../aop/aspectj/AbstractAspectJAdvice.java | 10 +--------- .../AspectJAdviceParameterNameDiscoverer.java | 16 ++-------------- .../aop/aspectj/AspectJProxyUtils.java | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index 7515334928f3..ab118f59a517 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -351,15 +351,7 @@ protected Class getDiscoveredThrowingType() { } private static boolean isVariableName(String name) { - if (!Character.isJavaIdentifierStart(name.charAt(0))) { - return false; - } - for (int i = 1; i < name.length(); i++) { - if (!Character.isJavaIdentifierPart(name.charAt(i))) { - return false; - } - } - return true; + return AspectJProxyUtils.isVariableName(name); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java index be072a1a583d..ffcea9d0b0c7 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java @@ -470,22 +470,10 @@ else if (numAnnotationSlots == 1) { */ @Nullable private String maybeExtractVariableName(@Nullable String candidateToken) { - if (!StringUtils.hasLength(candidateToken)) { - return null; - } - if (Character.isJavaIdentifierStart(candidateToken.charAt(0)) && - Character.isLowerCase(candidateToken.charAt(0))) { - for (int i = 1; i < candidateToken.length(); i++) { - char tokenChar = candidateToken.charAt(i); - if (!Character.isJavaIdentifierPart(tokenChar)) { - return null; - } - } + if (AspectJProxyUtils.isVariableName(candidateToken)) { return candidateToken; } - else { - return null; - } + return null; } /** diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java index 833a109f131e..5c0bc7c998cb 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java @@ -21,6 +21,7 @@ import org.springframework.aop.Advisor; import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.util.StringUtils; /** * Utility methods for working with AspectJ proxies. @@ -73,4 +74,18 @@ private static boolean isAspectJAdvice(Advisor advisor) { ((PointcutAdvisor) advisor).getPointcut() instanceof AspectJExpressionPointcut)); } + static boolean isVariableName(String name) { + if (!StringUtils.hasLength(name)) { + return false; + } + if (!Character.isJavaIdentifierStart(name.charAt(0))) { + return false; + } + for (int i = 1; i < name.length(); i++) { + if (!Character.isJavaIdentifierPart(name.charAt(i))) { + return false; + } + } + return true; + } } From 4cc831238c825c6d361d35ebf2563603da5eccae Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 18 Nov 2020 15:30:20 +0100 Subject: [PATCH 0041/1294] Revise Servlet 4 HttpServletMapping check Closes gh-26112 --- .../web/util/UrlPathHelper.java | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index 8c23248ddfa3..c760ea44c56b 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -22,6 +22,7 @@ import java.util.Properties; import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.MappingMatch; @@ -60,9 +61,8 @@ public class UrlPathHelper { */ public static final String PATH_ATTRIBUTE = UrlPathHelper.class.getName() + ".PATH"; - private static final boolean isServlet4Present = - ClassUtils.isPresent("javax.servlet.http.HttpServletMapping", - UrlPathHelper.class.getClassLoader()); + private static final boolean servlet4Present = + ClassUtils.hasMethod(HttpServletRequest.class, "getHttpServletMapping"); /** * Special WebSphere request attribute, indicating the original request URI. @@ -260,12 +260,15 @@ public String getLookupPathForRequest(HttpServletRequest request) { } } + /** + * Check whether servlet path determination can be skipped for the given request. + * @param request current HTTP request + * @return {@code true} if the request mapping has not been achieved using a path + * or if the servlet has been mapped to root; {@code false} otherwise + */ private boolean skipServletPathDetermination(HttpServletRequest request) { - if (isServlet4Present) { - if (request.getHttpServletMapping().getMappingMatch() != null) { - return !request.getHttpServletMapping().getMappingMatch().equals(MappingMatch.PATH) || - request.getHttpServletMapping().getPattern().equals("/*"); - } + if (servlet4Present) { + return Servlet4Delegate.skipServletPathDetermination(request); } return false; } @@ -763,4 +766,18 @@ public String removeSemicolonContent(String requestUri) { rawPathInstance.setReadOnly(); } + + /** + * Inner class to avoid a hard dependency on Servlet 4 {@link HttpServletMapping} + * and {@link MappingMatch} at runtime. + */ + private static class Servlet4Delegate { + + public static boolean skipServletPathDetermination(HttpServletRequest request) { + HttpServletMapping mapping = request.getHttpServletMapping(); + MappingMatch match = mapping.getMappingMatch(); + return (match != null && (!match.equals(MappingMatch.PATH) || mapping.getPattern().equals("/*"))); + } + } + } From ae75db265704103eab8bad8ca388d2d1cd9ed846 Mon Sep 17 00:00:00 2001 From: Benjamin Faal Date: Wed, 18 Nov 2020 18:41:59 +0100 Subject: [PATCH 0042/1294] Add allowedOriginPatterns to SockJS config See gh-26108 --- .../annotation/SockJsServiceRegistration.java | 15 +++++++ .../StompWebSocketEndpointRegistration.java | 7 ++++ ...MvcStompWebSocketEndpointRegistration.java | 22 +++++++++- .../support/OriginHandshakeInterceptor.java | 41 +++++++++++++++---- .../sockjs/support/AbstractSockJsService.java | 23 +++++++++++ ...ompWebSocketEndpointRegistrationTests.java | 26 ++++++++++++ 6 files changed, 126 insertions(+), 8 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/SockJsServiceRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/SockJsServiceRegistration.java index 7b184f11bcc2..4342fd2bfb0a 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/SockJsServiceRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/SockJsServiceRegistration.java @@ -73,6 +73,8 @@ public class SockJsServiceRegistration { private final List allowedOrigins = new ArrayList<>(); + private final List allowedOriginPatterns = new ArrayList<>(); + @Nullable private Boolean suppressCors; @@ -232,6 +234,18 @@ protected SockJsServiceRegistration setAllowedOrigins(String... allowedOrigins) return this; } + /** + * Configure allowed {@code Origin} pattern header values. + * @since 5.3.2 + */ + protected SockJsServiceRegistration setAllowedOriginPatterns(String... allowedOriginPatterns) { + this.allowedOriginPatterns.clear(); + if (!ObjectUtils.isEmpty(allowedOriginPatterns)) { + this.allowedOriginPatterns.addAll(Arrays.asList(allowedOriginPatterns)); + } + return this; + } + /** * This option can be used to disable automatic addition of CORS headers for * SockJS requests. @@ -284,6 +298,7 @@ protected SockJsService getSockJsService() { service.setSuppressCors(this.suppressCors); } service.setAllowedOrigins(this.allowedOrigins); + service.setAllowedOriginPatterns(this.allowedOriginPatterns); if (this.messageCodec != null) { service.setMessageCodec(this.messageCodec); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index 473964919793..3fcb8caeeedb 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -61,4 +61,11 @@ public interface StompWebSocketEndpointRegistration { */ StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); + /** + * Configure allowed {@code Origin} header values. + * + * @see org.springframework.web.cors.CorsConfiguration#setAllowedOriginPatterns(java.util.List) + */ + StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistration.java index d62ba0596e97..e9dcb84b7a5d 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistration.java @@ -58,6 +58,8 @@ public class WebMvcStompWebSocketEndpointRegistration implements StompWebSocketE private final List allowedOrigins = new ArrayList<>(); + private final List allowedOriginPatterns = new ArrayList<>(); + @Nullable private SockJsServiceRegistration registration; @@ -97,6 +99,15 @@ public StompWebSocketEndpointRegistration setAllowedOrigins(String... allowedOri return this; } + @Override + public StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... allowedOriginPatterns) { + this.allowedOriginPatterns.clear(); + if (!ObjectUtils.isEmpty(allowedOriginPatterns)) { + this.allowedOriginPatterns.addAll(Arrays.asList(allowedOriginPatterns)); + } + return this; + } + @Override public SockJsServiceRegistration withSockJS() { this.registration = new SockJsServiceRegistration(); @@ -112,13 +123,22 @@ public SockJsServiceRegistration withSockJS() { if (!this.allowedOrigins.isEmpty()) { this.registration.setAllowedOrigins(StringUtils.toStringArray(this.allowedOrigins)); } + if (!this.allowedOriginPatterns.isEmpty()) { + this.registration.setAllowedOriginPatterns(StringUtils.toStringArray(this.allowedOriginPatterns)); + } return this.registration; } protected HandshakeInterceptor[] getInterceptors() { List interceptors = new ArrayList<>(this.interceptors.size() + 1); interceptors.addAll(this.interceptors); - interceptors.add(new OriginHandshakeInterceptor(this.allowedOrigins)); + OriginHandshakeInterceptor originHandshakeInterceptor = new OriginHandshakeInterceptor(this.allowedOrigins); + interceptors.add(originHandshakeInterceptor); + + if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { + originHandshakeInterceptor.setAllowedOriginPatterns(this.allowedOriginPatterns); + } + return interceptors.toArray(new HandshakeInterceptor[0]); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index f10ec90f8d15..12fbd8b6f341 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -16,11 +16,12 @@ package org.springframework.web.socket.server.support; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; +import java.util.HashSet; +import java.util.List; import java.util.Map; -import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -30,6 +31,7 @@ import org.springframework.http.server.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import org.springframework.web.util.WebUtils; @@ -45,7 +47,7 @@ public class OriginHandshakeInterceptor implements HandshakeInterceptor { protected final Log logger = LogFactory.getLog(getClass()); - private final Set allowedOrigins = new LinkedHashSet<>(); + private final CorsConfiguration corsConfiguration = new CorsConfiguration(); /** @@ -74,8 +76,7 @@ public OriginHandshakeInterceptor(Collection allowedOrigins) { */ public void setAllowedOrigins(Collection allowedOrigins) { Assert.notNull(allowedOrigins, "Allowed origins Collection must not be null"); - this.allowedOrigins.clear(); - this.allowedOrigins.addAll(allowedOrigins); + this.corsConfiguration.setAllowedOrigins(new ArrayList<>(allowedOrigins)); } /** @@ -84,7 +85,33 @@ public void setAllowedOrigins(Collection allowedOrigins) { * @see #setAllowedOrigins */ public Collection getAllowedOrigins() { - return Collections.unmodifiableSet(this.allowedOrigins); + if (this.corsConfiguration.getAllowedOrigins() == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableSet(new HashSet<>(this.corsConfiguration.getAllowedOrigins())); + } + + /** + * Configure allowed {@code Origin} pattern header values. + * + * @see CorsConfiguration#setAllowedOriginPatterns(List) + */ + public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { + Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); + this.corsConfiguration.setAllowedOriginPatterns(new ArrayList<>(allowedOriginPatterns)); + } + + /** + * Return the allowed {@code Origin} pattern header values. + * + * @since 5.3.2 + * @see CorsConfiguration#getAllowedOriginPatterns() + */ + public Collection getAllowedOriginPatterns() { + if (this.corsConfiguration.getAllowedOriginPatterns() == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableSet(new HashSet<>(this.corsConfiguration.getAllowedOriginPatterns())); } @@ -92,7 +119,7 @@ public Collection getAllowedOrigins() { public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { - if (!WebUtils.isSameOrigin(request) && !WebUtils.isValidOrigin(request, this.allowedOrigins)) { + if (!WebUtils.isSameOrigin(request) && this.corsConfiguration.checkOrigin(request.getHeaders().getOrigin()) == null) { response.setStatusCode(HttpStatus.FORBIDDEN); if (logger.isDebugEnabled()) { logger.debug("Handshake request rejected, Origin header value " + diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 5dc84bbab063..7d04d82c02d5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -99,6 +99,8 @@ public abstract class AbstractSockJsService implements SockJsService, CorsConfig protected final Set allowedOrigins = new LinkedHashSet<>(); + protected final Set allowedOriginPatterns = new LinkedHashSet<>(); + private final SockJsRequestHandler infoHandler = new InfoHandler(); private final SockJsRequestHandler iframeHandler = new IframeHandler(); @@ -319,6 +321,17 @@ public void setAllowedOrigins(Collection allowedOrigins) { this.allowedOrigins.addAll(allowedOrigins); } + /** + * Configure allowed {@code Origin} header values. + * + * @see org.springframework.web.cors.CorsConfiguration#setAllowedOriginPatterns(java.util.List) + */ + public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { + Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); + this.allowedOriginPatterns.clear(); + this.allowedOriginPatterns.addAll(allowedOriginPatterns); + } + /** * Return configure allowed {@code Origin} header values. * @since 4.1.2 @@ -328,6 +341,15 @@ public Collection getAllowedOrigins() { return Collections.unmodifiableSet(this.allowedOrigins); } + /** + * Return configure allowed {@code Origin} pattern header values. + * @since 5.3.2 + * @see #setAllowedOriginPatterns + */ + public Collection getAllowedOriginPatterns() { + return Collections.unmodifiableSet(this.allowedOriginPatterns); + } + /** * This method determines the SockJS path and handles SockJS static URLs. @@ -498,6 +520,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { if (!this.suppressCors && (request.getHeader(HttpHeaders.ORIGIN) != null)) { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(new ArrayList<>(this.allowedOrigins)); + config.setAllowedOriginPatterns(new ArrayList<>(this.allowedOriginPatterns)); config.addAllowedMethod("*"); config.setAllowCredentials(true); config.setMaxAge(ONE_YEAR); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistrationTests.java index 9a5fe7945f04..eeab9c04e4da 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistrationTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistrationTests.java @@ -135,6 +135,32 @@ public void allowedOriginsWithSockJsService() { assertThat(sockJsService.shouldSuppressCors()).isFalse(); } + @Test + public void allowedOriginPatterns() { + WebMvcStompWebSocketEndpointRegistration registration = + new WebMvcStompWebSocketEndpointRegistration(new String[] {"/foo"}, this.handler, this.scheduler); + + String origin = "https://*.mydomain.com/"; + registration.setAllowedOriginPatterns(origin).withSockJS(); + + MultiValueMap mappings = registration.getMappings(); + assertThat(mappings.size()).isEqualTo(1); + SockJsHttpRequestHandler requestHandler = (SockJsHttpRequestHandler)mappings.entrySet().iterator().next().getKey(); + assertThat(requestHandler.getSockJsService()).isNotNull(); + DefaultSockJsService sockJsService = (DefaultSockJsService)requestHandler.getSockJsService(); + assertThat(sockJsService.getAllowedOriginPatterns().contains(origin)).isTrue(); + + registration = + new WebMvcStompWebSocketEndpointRegistration(new String[] {"/foo"}, this.handler, this.scheduler); + registration.withSockJS().setAllowedOriginPatterns(origin); + mappings = registration.getMappings(); + assertThat(mappings.size()).isEqualTo(1); + requestHandler = (SockJsHttpRequestHandler)mappings.entrySet().iterator().next().getKey(); + assertThat(requestHandler.getSockJsService()).isNotNull(); + sockJsService = (DefaultSockJsService)requestHandler.getSockJsService(); + assertThat(sockJsService.getAllowedOriginPatterns().contains(origin)).isTrue(); + } + @Test // SPR-12283 public void disableCorsWithSockJsService() { WebMvcStompWebSocketEndpointRegistration registration = From 9beca064047bdb4b55e24414f07a3190524bc801 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 18 Nov 2020 20:25:39 +0000 Subject: [PATCH 0043/1294] Polishing contribution See gh-26108 --- .../annotation/SockJsServiceRegistration.java | 2 +- .../StompWebSocketEndpointRegistration.java | 10 ++++--- ...MvcStompWebSocketEndpointRegistration.java | 8 +++--- .../support/OriginHandshakeInterceptor.java | 27 ++++++++++--------- .../sockjs/support/AbstractSockJsService.java | 9 ++++--- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/SockJsServiceRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/SockJsServiceRegistration.java index 4342fd2bfb0a..0a49af7309e4 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/SockJsServiceRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/SockJsServiceRegistration.java @@ -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. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java index 3fcb8caeeedb..d38d3caa7817 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/StompWebSocketEndpointRegistration.java @@ -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. @@ -62,9 +62,11 @@ public interface StompWebSocketEndpointRegistration { StompWebSocketEndpointRegistration setAllowedOrigins(String... origins); /** - * Configure allowed {@code Origin} header values. - * - * @see org.springframework.web.cors.CorsConfiguration#setAllowedOriginPatterns(java.util.List) + * A variant of {@link #setAllowedOrigins(String...)} that accepts flexible + * domain patterns, e.g. {@code "https://*.domain1.com/"}. Furthermore it + * always sets the {@code Access-Control-Allow-Origin} response header to + * the matched origin and never to {@code "*"}, nor to any other pattern. + * @since 5.3.2 */ StompWebSocketEndpointRegistration setAllowedOriginPatterns(String... originPatterns); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistration.java index e9dcb84b7a5d..3ab66b6bc281 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebMvcStompWebSocketEndpointRegistration.java @@ -132,13 +132,11 @@ public SockJsServiceRegistration withSockJS() { protected HandshakeInterceptor[] getInterceptors() { List interceptors = new ArrayList<>(this.interceptors.size() + 1); interceptors.addAll(this.interceptors); - OriginHandshakeInterceptor originHandshakeInterceptor = new OriginHandshakeInterceptor(this.allowedOrigins); - interceptors.add(originHandshakeInterceptor); - + OriginHandshakeInterceptor interceptor = new OriginHandshakeInterceptor(this.allowedOrigins); + interceptors.add(interceptor); if (!ObjectUtils.isEmpty(this.allowedOriginPatterns)) { - originHandshakeInterceptor.setAllowedOriginPatterns(this.allowedOriginPatterns); + interceptor.setAllowedOriginPatterns(this.allowedOriginPatterns); } - return interceptors.toArray(new HandshakeInterceptor[0]); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 12fbd8b6f341..69bd7833d269 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -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. @@ -82,18 +82,19 @@ public void setAllowedOrigins(Collection allowedOrigins) { /** * Return the allowed {@code Origin} header values. * @since 4.1.5 - * @see #setAllowedOrigins */ public Collection getAllowedOrigins() { - if (this.corsConfiguration.getAllowedOrigins() == null) { - return Collections.emptyList(); - } - return Collections.unmodifiableSet(new HashSet<>(this.corsConfiguration.getAllowedOrigins())); + return (this.corsConfiguration.getAllowedOrigins() != null ? + Collections.unmodifiableSet(new HashSet<>(this.corsConfiguration.getAllowedOrigins())) : + Collections.emptyList()); } /** - * Configure allowed {@code Origin} pattern header values. - * + * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible + * domain patterns, e.g. {@code "https://*.domain1.com/"}. Furthermore it + * always sets the {@code Access-Control-Allow-Origin} response header to + * the matched origin and never to {@code "*"}, nor to any other pattern. + * @since 5.3.2 * @see CorsConfiguration#setAllowedOriginPatterns(List) */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { @@ -108,10 +109,9 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { - if (this.corsConfiguration.getAllowedOriginPatterns() == null) { - return Collections.emptyList(); - } - return Collections.unmodifiableSet(new HashSet<>(this.corsConfiguration.getAllowedOriginPatterns())); + return (this.corsConfiguration.getAllowedOriginPatterns() != null ? + Collections.unmodifiableSet(new HashSet<>(this.corsConfiguration.getAllowedOriginPatterns())) : + Collections.emptyList()); } @@ -119,7 +119,8 @@ public Collection getAllowedOriginPatterns() { public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { - if (!WebUtils.isSameOrigin(request) && this.corsConfiguration.checkOrigin(request.getHeaders().getOrigin()) == null) { + if (!WebUtils.isSameOrigin(request) && + this.corsConfiguration.checkOrigin(request.getHeaders().getOrigin()) == null) { response.setStatusCode(HttpStatus.FORBIDDEN); if (logger.isDebugEnabled()) { logger.debug("Handshake request rejected, Origin header value " + diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 7d04d82c02d5..3cd840704727 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -322,9 +322,12 @@ public void setAllowedOrigins(Collection allowedOrigins) { } /** - * Configure allowed {@code Origin} header values. - * - * @see org.springframework.web.cors.CorsConfiguration#setAllowedOriginPatterns(java.util.List) + * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible + * domain patterns, e.g. {@code "https://*.domain1.com/"}. Furthermore it + * always sets the {@code Access-Control-Allow-Origin} response header to + * the matched origin and never to {@code "*"}, nor to any other pattern. + *

    By default this is not set. + * @since 5.2.3 */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); From 8130bf505fa69a00e0d566db36e543a9cf030683 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 18 Nov 2020 20:57:49 +0000 Subject: [PATCH 0044/1294] Apply allowedOriginPatterns in SockJsService See gh-26108 --- .../sockjs/support/AbstractSockJsService.java | 62 ++++++++++--------- .../TransportHandlingSockJsService.java | 3 +- .../sockjs/support/SockJsServiceTests.java | 4 +- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index 3cd840704727..66d2522acd62 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -25,7 +25,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Random; -import java.util.Set; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; @@ -97,9 +96,7 @@ public abstract class AbstractSockJsService implements SockJsService, CorsConfig private boolean suppressCors = false; - protected final Set allowedOrigins = new LinkedHashSet<>(); - - protected final Set allowedOriginPatterns = new LinkedHashSet<>(); + protected final CorsConfiguration corsConfiguration; private final SockJsRequestHandler infoHandler = new InfoHandler(); @@ -109,6 +106,18 @@ public abstract class AbstractSockJsService implements SockJsService, CorsConfig public AbstractSockJsService(TaskScheduler scheduler) { Assert.notNull(scheduler, "TaskScheduler must not be null"); this.taskScheduler = scheduler; + this.corsConfiguration = initCorsConfiguration(); + } + + private static CorsConfiguration initCorsConfiguration() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedMethod("*"); + config.setAllowedOrigins(Collections.emptyList()); + config.setAllowedOriginPatterns(Collections.emptyList()); + config.setAllowCredentials(true); + config.setMaxAge(ONE_YEAR); + config.addAllowedHeader("*"); + return config; } @@ -317,10 +326,18 @@ public boolean shouldSuppressCors() { */ public void setAllowedOrigins(Collection allowedOrigins) { Assert.notNull(allowedOrigins, "Allowed origins Collection must not be null"); - this.allowedOrigins.clear(); - this.allowedOrigins.addAll(allowedOrigins); + this.corsConfiguration.setAllowedOrigins(new ArrayList<>(allowedOrigins)); } + /** + * Return configure allowed {@code Origin} header values. + * @since 4.1.2 + * @see #setAllowedOrigins + */ + @SuppressWarnings("ConstantConditions") + public Collection getAllowedOrigins() { + return this.corsConfiguration.getAllowedOrigins(); + } /** * A variant of {@link #setAllowedOrigins(Collection)} that accepts flexible * domain patterns, e.g. {@code "https://*.domain1.com/"}. Furthermore it @@ -331,26 +348,17 @@ public void setAllowedOrigins(Collection allowedOrigins) { */ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { Assert.notNull(allowedOriginPatterns, "Allowed origin patterns Collection must not be null"); - this.allowedOriginPatterns.clear(); - this.allowedOriginPatterns.addAll(allowedOriginPatterns); - } - - /** - * Return configure allowed {@code Origin} header values. - * @since 4.1.2 - * @see #setAllowedOrigins - */ - public Collection getAllowedOrigins() { - return Collections.unmodifiableSet(this.allowedOrigins); + this.corsConfiguration.setAllowedOriginPatterns(new ArrayList<>(allowedOriginPatterns)); } /** - * Return configure allowed {@code Origin} pattern header values. + * Return {@link #setAllowedOriginPatterns(Collection) configured} origin patterns. * @since 5.3.2 * @see #setAllowedOriginPatterns */ + @SuppressWarnings("ConstantConditions") public Collection getAllowedOriginPatterns() { - return Collections.unmodifiableSet(this.allowedOriginPatterns); + return this.corsConfiguration.getAllowedOriginPatterns(); } @@ -396,7 +404,8 @@ else if (sockJsPath.equals("/info")) { } else if (sockJsPath.matches("/iframe[0-9-.a-z_]*.html")) { - if (!this.allowedOrigins.isEmpty() && !this.allowedOrigins.contains("*")) { + if (!getAllowedOrigins().isEmpty() && !getAllowedOrigins().contains("*") || + !getAllowedOriginPatterns().isEmpty()) { if (requestInfo != null) { logger.debug("Iframe support is disabled when an origin check is required. " + "Ignoring transport request: " + requestInfo); @@ -404,7 +413,7 @@ else if (sockJsPath.matches("/iframe[0-9-.a-z_]*.html")) { response.setStatusCode(HttpStatus.NOT_FOUND); return; } - if (this.allowedOrigins.isEmpty()) { + if (getAllowedOrigins().isEmpty()) { response.getHeaders().add(XFRAME_OPTIONS_HEADER, "SAMEORIGIN"); } if (requestInfo != null) { @@ -506,7 +515,7 @@ protected boolean checkOrigin(ServerHttpRequest request, ServerHttpResponse resp return true; } - if (!WebUtils.isValidOrigin(request, this.allowedOrigins)) { + if (this.corsConfiguration.checkOrigin(request.getHeaders().getOrigin()) == null) { if (logger.isWarnEnabled()) { logger.warn("Origin header value '" + request.getHeaders().getOrigin() + "' not allowed."); } @@ -521,14 +530,7 @@ protected boolean checkOrigin(ServerHttpRequest request, ServerHttpResponse resp @Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { if (!this.suppressCors && (request.getHeader(HttpHeaders.ORIGIN) != null)) { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(new ArrayList<>(this.allowedOrigins)); - config.setAllowedOriginPatterns(new ArrayList<>(this.allowedOriginPatterns)); - config.addAllowedMethod("*"); - config.setAllowCredentials(true); - config.setMaxAge(ONE_YEAR); - config.addAllowedHeader("*"); - return config; + return this.corsConfiguration; } return null; } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java index 272700b49a44..8968980ef6ae 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java @@ -344,7 +344,8 @@ protected boolean validateRequest(String serverId, String sessionId, String tran return false; } - if (!this.allowedOrigins.contains("*")) { + if (!getAllowedOrigins().isEmpty() && !getAllowedOrigins().contains("*") || + !getAllowedOriginPatterns().isEmpty()) { TransportType transportType = TransportType.fromValue(transport); if (transportType == null || !transportType.supportsOrigin()) { if (logger.isWarnEnabled()) { diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/SockJsServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/SockJsServiceTests.java index f34c2f8e920f..16106bdccc85 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/SockJsServiceTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/SockJsServiceTests.java @@ -215,7 +215,7 @@ public void handleInfoOptionsWithForbiddenOrigin() { @Test // SPR-12283 public void handleInfoOptionsWithOriginAndCorsHeadersDisabled() { this.servletRequest.addHeader(HttpHeaders.ORIGIN, "https://mydomain2.example/"); - this.service.setAllowedOrigins(Collections.singletonList("*")); + this.service.setAllowedOriginPatterns(Collections.singletonList("*")); this.service.setSuppressCors(true); this.servletRequest.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Last-Modified"); @@ -223,10 +223,12 @@ public void handleInfoOptionsWithOriginAndCorsHeadersDisabled() { assertThat(this.service.getCorsConfiguration(this.servletRequest)).isNull(); this.service.setAllowedOrigins(Collections.singletonList("https://mydomain1.example/")); + this.service.setAllowedOriginPatterns(Collections.emptyList()); resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.FORBIDDEN); assertThat(this.service.getCorsConfiguration(this.servletRequest)).isNull(); this.service.setAllowedOrigins(Arrays.asList("https://mydomain1.example/", "https://mydomain2.example/", "http://mydomain3.example/")); + this.service.setAllowedOriginPatterns(Collections.emptyList()); resetResponseAndHandleRequest("OPTIONS", "/echo/info", HttpStatus.NO_CONTENT); assertThat(this.service.getCorsConfiguration(this.servletRequest)).isNull(); } From 684e695b08a25124ed6632101bc1f6d8ba1777f4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 18 Nov 2020 21:20:38 +0000 Subject: [PATCH 0045/1294] Expose allowedOriginPatterns in SocketJS XML config Closes gh-26108 --- .../config/HandlersBeanDefinitionParser.java | 11 ++++++++-- .../MessageBrokerBeanDefinitionParser.java | 9 ++++++++- .../config/WebSocketNamespaceUtils.java | 9 ++++++++- .../web/socket/config/spring-websocket.xsd | 20 +++++++++++++++++++ .../HandlersBeanDefinitionParserTests.java | 6 +++--- ...essageBrokerBeanDefinitionParserTests.java | 4 ++-- .../config/websocket-config-broker-simple.xml | 8 ++++++-- ...cket-config-handlers-sockjs-attributes.xml | 2 +- 8 files changed, 57 insertions(+), 12 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/HandlersBeanDefinitionParser.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/HandlersBeanDefinitionParser.java index 367954698710..f3db3ba3ce9b 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/HandlersBeanDefinitionParser.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/HandlersBeanDefinitionParser.java @@ -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. @@ -32,6 +32,7 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.springframework.web.socket.server.support.OriginHandshakeInterceptor; @@ -85,7 +86,13 @@ public BeanDefinition parse(Element element, ParserContext context) { ManagedList interceptors = WebSocketNamespaceUtils.parseBeanSubElements(interceptElem, context); String allowedOrigins = element.getAttribute("allowed-origins"); List origins = Arrays.asList(StringUtils.tokenizeToStringArray(allowedOrigins, ",")); - interceptors.add(new OriginHandshakeInterceptor(origins)); + String allowedOriginPatterns = element.getAttribute("allowed-origin-patterns"); + List originPatterns = Arrays.asList(StringUtils.tokenizeToStringArray(allowedOriginPatterns, ",")); + OriginHandshakeInterceptor interceptor = new OriginHandshakeInterceptor(origins); + if (!ObjectUtils.isEmpty(originPatterns)) { + interceptor.setAllowedOriginPatterns(originPatterns); + } + interceptors.add(interceptor); strategy = new WebSocketHandlerMappingStrategy(handler, interceptors); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java index 9e9b5b4c8b0b..a0828f3f37bc 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParser.java @@ -60,6 +60,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MimeTypeUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; import org.springframework.web.socket.WebSocketHandler; @@ -358,7 +359,13 @@ private RuntimeBeanReference registerRequestHandler( ManagedList interceptors = WebSocketNamespaceUtils.parseBeanSubElements(interceptElem, ctx); String allowedOrigins = element.getAttribute("allowed-origins"); List origins = Arrays.asList(StringUtils.tokenizeToStringArray(allowedOrigins, ",")); - interceptors.add(new OriginHandshakeInterceptor(origins)); + String allowedOriginPatterns = element.getAttribute("allowed-origin-patterns"); + List originPatterns = Arrays.asList(StringUtils.tokenizeToStringArray(allowedOriginPatterns, ",")); + OriginHandshakeInterceptor interceptor = new OriginHandshakeInterceptor(origins); + if (!ObjectUtils.isEmpty(originPatterns)) { + interceptor.setAllowedOriginPatterns(originPatterns); + } + interceptors.add(interceptor); ConstructorArgumentValues cargs = new ConstructorArgumentValues(); cargs.addIndexedArgumentValue(0, subProtoHandler); cargs.addIndexedArgumentValue(1, handler); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketNamespaceUtils.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketNamespaceUtils.java index c826febd2316..9ec317e37fad 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketNamespaceUtils.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketNamespaceUtils.java @@ -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. @@ -105,11 +105,18 @@ else if (handshakeHandler != null) { Element interceptElem = DomUtils.getChildElementByTagName(element, "handshake-interceptors"); ManagedList interceptors = WebSocketNamespaceUtils.parseBeanSubElements(interceptElem, context); + String allowedOrigins = element.getAttribute("allowed-origins"); List origins = Arrays.asList(StringUtils.tokenizeToStringArray(allowedOrigins, ",")); sockJsServiceDef.getPropertyValues().add("allowedOrigins", origins); + + String allowedOriginPatterns = element.getAttribute("allowed-origin-patterns"); + List originPatterns = Arrays.asList(StringUtils.tokenizeToStringArray(allowedOriginPatterns, ",")); + sockJsServiceDef.getPropertyValues().add("allowedOriginPatterns", originPatterns); + RootBeanDefinition originHandshakeInterceptor = new RootBeanDefinition(OriginHandshakeInterceptor.class); originHandshakeInterceptor.getPropertyValues().add("allowedOrigins", origins); + originHandshakeInterceptor.getPropertyValues().add("allowedOriginPatterns", originPatterns); interceptors.add(originHandshakeInterceptor); sockJsServiceDef.getPropertyValues().add("handshakeInterceptors", interceptors); diff --git a/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket.xsd b/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket.xsd index 923a3b9f45ad..1870ad7ee5ab 100644 --- a/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket.xsd +++ b/spring-websocket/src/main/resources/org/springframework/web/socket/config/spring-websocket.xsd @@ -541,6 +541,16 @@ ]]> + + + + + @@ -739,6 +749,16 @@ ]]> + + + + + diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/config/HandlersBeanDefinitionParserTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/config/HandlersBeanDefinitionParserTests.java index 27024631adc3..6bf407c83a6e 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/config/HandlersBeanDefinitionParserTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/config/HandlersBeanDefinitionParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -226,8 +226,8 @@ public void sockJsAttributes() { List interceptors = transportService.getHandshakeInterceptors(); assertThat(interceptors).extracting("class").containsExactly(OriginHandshakeInterceptor.class); assertThat(transportService.shouldSuppressCors()).isTrue(); - assertThat(transportService.getAllowedOrigins().contains("https://mydomain1.example/")).isTrue(); - assertThat(transportService.getAllowedOrigins().contains("https://mydomain2.example/")).isTrue(); + assertThat(transportService.getAllowedOrigins()).containsExactly("https://mydomain1.example/", "https://mydomain2.example/"); + assertThat(transportService.getAllowedOriginPatterns()).containsExactly("https://*.mydomain.example/"); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java index af5df574ae30..fda305a34f54 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/config/MessageBrokerBeanDefinitionParserTests.java @@ -181,8 +181,8 @@ public void simpleBroker() throws Exception { interceptors = defaultSockJsService.getHandshakeInterceptors(); assertThat(interceptors).extracting("class").containsExactly(FooTestInterceptor.class, BarTestInterceptor.class, OriginHandshakeInterceptor.class); - assertThat(defaultSockJsService.getAllowedOrigins().contains("https://mydomain3.com/")).isTrue(); - assertThat(defaultSockJsService.getAllowedOrigins().contains("https://mydomain4.com/")).isTrue(); + assertThat(defaultSockJsService.getAllowedOrigins()).containsExactly("https://mydomain3.com/", "https://mydomain4.com/"); + assertThat(defaultSockJsService.getAllowedOriginPatterns()).containsExactly("https://*.mydomain.com/"); SimpUserRegistry userRegistry = this.appContext.getBean(SimpUserRegistry.class); assertThat(userRegistry).isNotNull(); diff --git a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml index 66301e6be451..4f640331f43e 100644 --- a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml +++ b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-broker-simple.xml @@ -17,7 +17,9 @@ - + @@ -25,7 +27,9 @@ - + diff --git a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs-attributes.xml b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs-attributes.xml index 308de21259ad..fd93cd90057e 100644 --- a/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs-attributes.xml +++ b/spring-websocket/src/test/resources/org/springframework/web/socket/config/websocket-config-handlers-sockjs-attributes.xml @@ -5,7 +5,7 @@ http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd"> - + Date: Thu, 19 Nov 2020 09:20:55 +0000 Subject: [PATCH 0046/1294] Allow "*" for Access-Control-Expose-Headers Closes gh-26113 --- .../web/bind/annotation/CrossOrigin.java | 2 ++ .../web/cors/CorsConfiguration.java | 12 +++------ .../web/cors/CorsConfigurationTests.java | 27 +++++++++---------- .../web/reactive/config/CorsRegistration.java | 3 ++- .../config/annotation/CorsRegistration.java | 3 ++- .../web/servlet/config/spring-mvc.xsd | 1 + 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java index ca7550497839..bb49357aab40 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java @@ -114,6 +114,8 @@ * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, *

    Exposed headers are listed in the {@code Access-Control-Expose-Headers} * response header of actual CORS requests. + *

    The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. *

    By default no headers are listed as exposed. */ String[] exposedHeaders() default {}; diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index 6ca27ac9bab2..68252e68933b 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -329,13 +329,11 @@ else if (this.allowedHeaders == DEFAULT_PERMIT_ALL) { * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, * {@code Expires}, {@code Last-Modified}, or {@code Pragma}) that an * actual response might have and can be exposed. - *

    Note that {@code "*"} is not a valid exposed header value. + *

    The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. *

    By default this is not set. */ public void setExposedHeaders(@Nullable List exposedHeaders) { - if (exposedHeaders != null && exposedHeaders.contains(ALL)) { - throw new IllegalArgumentException("'*' is not a valid exposed header value"); - } this.exposedHeaders = (exposedHeaders != null ? new ArrayList<>(exposedHeaders) : null); } @@ -351,12 +349,10 @@ public List getExposedHeaders() { /** * Add a response header to expose. - *

    Note that {@code "*"} is not a valid exposed header value. + *

    The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. */ public void addExposedHeader(String exposedHeader) { - if (ALL.equals(exposedHeader)) { - throw new IllegalArgumentException("'*' is not a valid exposed header value"); - } if (this.exposedHeaders == null) { this.exposedHeaders = new ArrayList<>(4); } diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 446fd81c013e..82c5286dce7b 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -61,8 +61,7 @@ public void setValues() { config.addAllowedOriginPattern("http://*.example.com/"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); - config.addExposedHeader("header1"); - config.addExposedHeader("header2"); + config.addExposedHeader("*"); config.setAllowCredentials(true); config.setMaxAge(123L); @@ -70,23 +69,11 @@ public void setValues() { assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.example.com/"); assertThat(config.getAllowedHeaders()).containsExactly("*"); assertThat(config.getAllowedMethods()).containsExactly("*"); - assertThat(config.getExposedHeaders()).containsExactly("header1", "header2"); + assertThat(config.getExposedHeaders()).containsExactly("*"); assertThat(config.getAllowCredentials()).isTrue(); assertThat(config.getMaxAge()).isEqualTo(new Long(123)); } - @Test - public void asteriskWildCardOnAddExposedHeader() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new CorsConfiguration().addExposedHeader("*")); - } - - @Test - public void asteriskWildCardOnSetExposedHeaders() { - assertThatIllegalArgumentException() - .isThrownBy(() -> new CorsConfiguration().setExposedHeaders(Collections.singletonList("*"))); - } - @Test public void combineWithNull() { CorsConfiguration config = new CorsConfiguration(); @@ -133,12 +120,14 @@ public void combineWithDefaultPermitValues() { assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com/"); assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1"); assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name()); + assertThat(combinedConfig.getExposedHeaders()).isEmpty(); combinedConfig = other.combine(config); assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedOrigins()).containsExactly("https://domain.com/"); assertThat(combinedConfig.getAllowedHeaders()).containsExactly("header1"); assertThat(combinedConfig.getAllowedMethods()).containsExactly(HttpMethod.PUT.name()); + assertThat(combinedConfig.getExposedHeaders()).isEmpty(); combinedConfig = config.combine(new CorsConfiguration()); assertThat(config.getAllowedOrigins()).containsExactly("*"); @@ -146,6 +135,7 @@ public void combineWithDefaultPermitValues() { assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedMethods()) .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + assertThat(combinedConfig.getExposedHeaders()).isEmpty(); combinedConfig = new CorsConfiguration().combine(config); assertThat(config.getAllowedOrigins()).containsExactly("*"); @@ -153,6 +143,7 @@ public void combineWithDefaultPermitValues() { assertThat(combinedConfig).isNotNull(); assertThat(combinedConfig.getAllowedMethods()) .containsExactly(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + assertThat(combinedConfig.getExposedHeaders()).isEmpty(); } @Test @@ -196,6 +187,7 @@ public void combineWithAsteriskWildCard() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); + config.addExposedHeader("*"); config.addAllowedMethod("*"); config.addAllowedOriginPattern("*"); @@ -204,6 +196,8 @@ public void combineWithAsteriskWildCard() { other.addAllowedOriginPattern("http://*.company.com/"); other.addAllowedHeader("header1"); other.addExposedHeader("header2"); + other.addAllowedHeader("anotherHeader1"); + other.addExposedHeader("anotherHeader2"); other.addAllowedMethod(HttpMethod.PUT.name()); CorsConfiguration combinedConfig = config.combine(other); @@ -211,6 +205,7 @@ public void combineWithAsteriskWildCard() { assertThat(combinedConfig.getAllowedOrigins()).containsExactly("*"); assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("*"); assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*"); + assertThat(combinedConfig.getExposedHeaders()).containsExactly("*"); assertThat(combinedConfig.getAllowedMethods()).containsExactly("*"); combinedConfig = other.combine(config); @@ -218,7 +213,9 @@ public void combineWithAsteriskWildCard() { assertThat(combinedConfig.getAllowedOrigins()).containsExactly("*"); assertThat(combinedConfig.getAllowedOriginPatterns()).containsExactly("*"); assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*"); + assertThat(combinedConfig.getExposedHeaders()).containsExactly("*"); assertThat(combinedConfig.getAllowedMethods()).containsExactly("*"); + assertThat(combinedConfig.getAllowedHeaders()).containsExactly("*"); } @Test // SPR-14792 diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java index d459eaef1e7e..ce7aa0130329 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/CorsRegistration.java @@ -98,7 +98,8 @@ public CorsRegistration allowedHeaders(String... headers) { * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an * actual response might have and can be exposed. - *

    Note that {@code "*"} is not supported on this property. + *

    The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. *

    By default this is not set. */ public CorsRegistration exposedHeaders(String... headers) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java index 71542289803b..0de5ecae5dbc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -99,7 +99,8 @@ public CorsRegistration allowedHeaders(String... headers) { * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type}, * {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an * actual response might have and can be exposed. - *

    Note that {@code "*"} is not supported on this property. + *

    The special value {@code "*"} allows all headers to be exposed for + * non-credentialed requests. *

    By default this is not set. */ public CorsRegistration exposedHeaders(String... headers) { diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd index b604caae745d..c0364d28c12f 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd @@ -1400,6 +1400,7 @@ Comma-separated list of response headers other than simple headers (i.e. Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma) that an actual response might have and can be exposed. + The special value "*" allows all headers to be exposed for non-credentialed requests. Empty by default. ]]> From 6a0377b1a257a10ddc5735472094e649c14e2572 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 19 Nov 2020 15:09:30 +0100 Subject: [PATCH 0047/1294] Upgrade to Tomcat 9.0.40 and Apache HttpClient 4.5.13 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d42039382e4b..3d43f195f601 100644 --- a/build.gradle +++ b/build.gradle @@ -128,14 +128,14 @@ configure(allprojects) { project -> dependency "org.webjars:webjars-locator-core:0.46" dependency "org.webjars:underscorejs:1.8.3" - dependencySet(group: 'org.apache.tomcat', version: '9.0.39') { + dependencySet(group: 'org.apache.tomcat', version: '9.0.40') { entry 'tomcat-util' entry('tomcat-websocket') { exclude group: "org.apache.tomcat", name: "tomcat-websocket-api" exclude group: "org.apache.tomcat", name: "tomcat-servlet-api" } } - dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.39') { + dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.40') { entry 'tomcat-embed-core' entry 'tomcat-embed-websocket' } @@ -154,7 +154,7 @@ configure(allprojects) { project -> entry 'okhttp' entry 'mockwebserver' } - dependency("org.apache.httpcomponents:httpclient:4.5.12") { + dependency("org.apache.httpcomponents:httpclient:4.5.13") { exclude group: "commons-logging", name: "commons-logging" } dependency("org.apache.httpcomponents:httpasyncclient:4.1.4") { From 135682e073d307b0bc35d235f6e1ba2fe60b039d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 19 Nov 2020 16:02:23 +0100 Subject: [PATCH 0048/1294] Upgrade to Hibernate ORM 5.4.24 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3d43f195f601..c41de3ccf215 100644 --- a/build.gradle +++ b/build.gradle @@ -123,7 +123,7 @@ configure(allprojects) { project -> dependency "net.sf.ehcache:ehcache:2.10.6" dependency "org.ehcache:jcache:1.0.1" dependency "org.ehcache:ehcache:3.4.0" - dependency "org.hibernate:hibernate-core:5.4.23.Final" + dependency "org.hibernate:hibernate-core:5.4.24.Final" dependency "org.hibernate:hibernate-validator:6.1.6.Final" dependency "org.webjars:webjars-locator-core:0.46" dependency "org.webjars:underscorejs:1.8.3" From 9ec96f6141a35f6615c5bc68bceac3facd44e8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=A6=D1=8B=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Thu, 19 Nov 2020 17:36:29 +0200 Subject: [PATCH 0049/1294] Fail MethodFilter.and() immediately when null is passed --- .../src/main/java/org/springframework/util/ReflectionUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java index e00fef835c66..8cd1b54202bc 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -831,9 +831,11 @@ public interface MethodFilter { *

    If this filter does not match, the next filter will not be applied. * @param next the next {@code MethodFilter} * @return a composite {@code MethodFilter} + * @throws IllegalArgumentException if method's argument is {@code null} * @since 5.3.2 */ default MethodFilter and(MethodFilter next) { + Assert.notNull(next, "Next MethodFilter must not be null!"); return method -> matches(method) && next.matches(method); } } From c92dccea1bab99b89375d5b6c0991b544c42a4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=A6=D1=8B=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Thu, 19 Nov 2020 17:29:16 +0200 Subject: [PATCH 0050/1294] Simplify XMLEventStreamWriter.writeEndElement() --- .../org/springframework/util/xml/XMLEventStreamWriter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java index 7ee10fee4f12..6c6d6de48859 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java +++ b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamWriter.java @@ -161,9 +161,8 @@ private void closeEmptyElementIfNecessary() throws XMLStreamException { public void writeEndElement() throws XMLStreamException { closeEmptyElementIfNecessary(); int last = this.endElements.size() - 1; - EndElement lastEndElement = this.endElements.get(last); + EndElement lastEndElement = this.endElements.remove(last); this.eventWriter.add(lastEndElement); - this.endElements.remove(last); } @Override From a0544e78ea2b4736122ff0dffaf6fe0c729b32a1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 19 Nov 2020 18:47:31 +0100 Subject: [PATCH 0051/1294] Replace early SpringProperties logger usage with System.err Closes gh-26120 --- .../org/springframework/core/SpringProperties.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java index 4026ade5cdc5..dc7470458b45 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -21,9 +21,6 @@ import java.net.URL; import java.util.Properties; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.lang.Nullable; /** @@ -50,8 +47,6 @@ public final class SpringProperties { private static final String PROPERTIES_RESOURCE_LOCATION = "spring.properties"; - private static final Log logger = LogFactory.getLog(SpringProperties.class); - private static final Properties localProperties = new Properties(); @@ -61,16 +56,13 @@ public final class SpringProperties { URL url = (cl != null ? cl.getResource(PROPERTIES_RESOURCE_LOCATION) : ClassLoader.getSystemResource(PROPERTIES_RESOURCE_LOCATION)); if (url != null) { - logger.debug("Found 'spring.properties' file in local classpath"); try (InputStream is = url.openStream()) { localProperties.load(is); } } } catch (IOException ex) { - if (logger.isInfoEnabled()) { - logger.info("Could not load 'spring.properties' file from local classpath: " + ex); - } + System.err.println("Could not load 'spring.properties' file from local classpath: " + ex); } } @@ -108,9 +100,7 @@ public static String getProperty(String key) { value = System.getProperty(key); } catch (Throwable ex) { - if (logger.isDebugEnabled()) { - logger.debug("Could not retrieve system property '" + key + "': " + ex); - } + System.err.println("Could not retrieve system property '" + key + "': " + ex); } } return value; From a7cce64e5d949477ab118b4c2c30a4c59b84bcaf Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 19 Nov 2020 21:05:24 +0100 Subject: [PATCH 0052/1294] Add assertion check to FieldFilter.and(FieldFilter) method as well See gh-26121 --- .../org/springframework/util/ReflectionUtils.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java index 8cd1b54202bc..6b457daaa360 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -826,16 +826,15 @@ public interface MethodFilter { boolean matches(Method method); /** - * Create a composite filter based on this filter and the provided - * filter. + * Create a composite filter based on this filter and the provided filter. *

    If this filter does not match, the next filter will not be applied. * @param next the next {@code MethodFilter} * @return a composite {@code MethodFilter} - * @throws IllegalArgumentException if method's argument is {@code null} + * @throws IllegalArgumentException if the MethodFilter argument is {@code null} * @since 5.3.2 */ default MethodFilter and(MethodFilter next) { - Assert.notNull(next, "Next MethodFilter must not be null!"); + Assert.notNull(next, "Next MethodFilter must not be null"); return method -> matches(method) && next.matches(method); } } @@ -868,14 +867,15 @@ public interface FieldFilter { boolean matches(Field field); /** - * Create a composite filter based on this filter and the provided - * filter. + * Create a composite filter based on this filter and the provided filter. *

    If this filter does not match, the next filter will not be applied. * @param next the next {@code FieldFilter} * @return a composite {@code FieldFilter} + * @throws IllegalArgumentException if the FieldFilter argument is {@code null} * @since 5.3.2 */ default FieldFilter and(FieldFilter next) { + Assert.notNull(next, "Next FieldFilter must not be null"); return field -> matches(field) && next.matches(field); } } From 05e3f271b62ffbecb2f1f0defe96d4959334843f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 19 Nov 2020 21:11:23 +0000 Subject: [PATCH 0053/1294] Consistently apply PrincipalMethodArgumentResolver Closes gh-26117 --- .../ExceptionHandlerExceptionResolver.java | 3 + .../RequestMappingHandlerAdapter.java | 1 + ...MappingHandlerAdapterIntegrationTests.java | 71 +++++++++++++++---- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index 15b504430dba..9d731f6101a6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -342,6 +342,9 @@ protected List getDefaultArgumentResolvers() { resolvers.addAll(getCustomArgumentResolvers()); } + // Catch-all + resolvers.add(new PrincipalMethodArgumentResolver()); + return resolvers; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 6b051220021a..9e5ea44432ec 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -681,6 +681,7 @@ private List getDefaultArgumentResolvers() { } // Catch-all + resolvers.add(new PrincipalMethodArgumentResolver()); resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java index 9aeddcaa566c..6c53d75cda91 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -16,7 +16,12 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.awt.Color; +import java.awt.*; +import java.lang.annotation.Documented; +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.net.URI; import java.security.Principal; @@ -70,6 +75,7 @@ import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.SessionStatus; import org.springframework.web.bind.support.WebArgumentResolver; +import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; @@ -77,6 +83,7 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.InvocableHandlerMethod; +import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; @@ -114,6 +121,7 @@ public void setup() throws Exception { List customResolvers = new ArrayList<>(); customResolvers.add(new ServletWebArgumentResolverAdapter(new ColorArgumentResolver())); + customResolvers.add(new CustomPrincipalArgumentResolver()); GenericWebApplicationContext context = new GenericWebApplicationContext(); context.refresh(); @@ -145,7 +153,7 @@ public void handle() throws Exception { Class[] parameterTypes = new Class[] {int.class, String.class, String.class, String.class, Map.class, Date.class, Map.class, String.class, String.class, TestBean.class, Errors.class, TestBean.class, Color.class, HttpServletRequest.class, HttpServletResponse.class, TestBean.class, TestBean.class, - User.class, OtherUser.class, Model.class, UriComponentsBuilder.class}; + User.class, OtherUser.class, Principal.class, Model.class, UriComponentsBuilder.class}; String datePattern = "yyyy.MM.dd"; String formattedDate = "2011.03.16"; @@ -214,6 +222,7 @@ public void handle() throws Exception { assertThat(model.get("customArg") instanceof Color).isTrue(); assertThat(model.get("user").getClass()).isEqualTo(User.class); assertThat(model.get("otherUser").getClass()).isEqualTo(OtherUser.class); + assertThat(((Principal) model.get("customUser")).getName()).isEqualTo("Custom User"); assertThat(model.get("sessionAttribute")).isSameAs(sessionAttribute); assertThat(model.get("requestAttribute")).isSameAs(requestAttribute); @@ -476,16 +485,24 @@ public String handle( HttpServletResponse response, @SessionAttribute TestBean sessionAttribute, @RequestAttribute TestBean requestAttribute, - User user, + @Nullable User user, // gh-26117, gh-26117 (for @Nullable) @ModelAttribute OtherUser otherUser, + @AuthenticationPrincipal Principal customUser, // gh-25780 Model model, UriComponentsBuilder builder) { - model.addAttribute("cookie", cookieV).addAttribute("pathvar", pathvarV).addAttribute("header", headerV) - .addAttribute("systemHeader", systemHeader).addAttribute("headerMap", headerMap) - .addAttribute("dateParam", dateParam).addAttribute("paramMap", paramMap) - .addAttribute("paramByConvention", paramByConvention).addAttribute("value", value) - .addAttribute("customArg", customArg).addAttribute(user) + model.addAttribute("cookie", cookieV) + .addAttribute("pathvar", pathvarV) + .addAttribute("header", headerV) + .addAttribute("systemHeader", systemHeader) + .addAttribute("headerMap", headerMap) + .addAttribute("dateParam", dateParam) + .addAttribute("paramMap", paramMap) + .addAttribute("paramByConvention", paramByConvention) + .addAttribute("value", value) + .addAttribute("customArg", customArg) + .addAttribute(user) + .addAttribute("customUser", customUser) .addAttribute("sessionAttribute", sessionAttribute) .addAttribute("requestAttribute", requestAttribute) .addAttribute("url", builder.path("/path").build().toUri()); @@ -520,10 +537,15 @@ public String handleInInterface( Model model, UriComponentsBuilder builder) { - model.addAttribute("cookie", cookieV).addAttribute("pathvar", pathvarV).addAttribute("header", headerV) - .addAttribute("systemHeader", systemHeader).addAttribute("headerMap", headerMap) - .addAttribute("dateParam", dateParam).addAttribute("paramMap", paramMap) - .addAttribute("paramByConvention", paramByConvention).addAttribute("value", value) + model.addAttribute("cookie", cookieV) + .addAttribute("pathvar", pathvarV) + .addAttribute("header", headerV) + .addAttribute("systemHeader", systemHeader) + .addAttribute("headerMap", headerMap) + .addAttribute("dateParam", dateParam) + .addAttribute("paramMap", paramMap) + .addAttribute("paramByConvention", paramByConvention) + .addAttribute("value", value) .addAttribute("customArg", customArg).addAttribute(user) .addAttribute("sessionAttribute", sessionAttribute) .addAttribute("requestAttribute", requestAttribute) @@ -598,7 +620,6 @@ public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest } } - private static class User implements Principal { @Override @@ -616,4 +637,26 @@ public String getName() { } } + private static class CustomPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return (Principal.class.isAssignableFrom(parameter.getParameterType()) && + parameter.hasParameterAnnotation(AuthenticationPrincipal.class)); + } + + @Nullable + @Override + public Object resolveArgument( + MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { + + return (Principal) () -> "Custom User"; + } + } + + @Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface AuthenticationPrincipal {} } From 922d5d271a33d3b7356bb855b14b008ccd31b727 Mon Sep 17 00:00:00 2001 From: "Spindler, Justin" Date: Wed, 18 Nov 2020 10:50:57 -0500 Subject: [PATCH 0054/1294] Add ResponseSpec#toEntityFlux overload with BodyExtractor See gh-26114 --- .../function/client/DefaultWebClient.java | 8 +++++ .../reactive/function/client/WebClient.java | 13 ++++++- .../client/DefaultWebClientTests.java | 2 ++ .../client/WebClientIntegrationTests.java | 34 +++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index db8b511927c9..b68888d0e332 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -43,11 +43,13 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.util.UriBuilder; @@ -599,6 +601,12 @@ public Mono>> toEntityFlux(ParameterizedTypeReference handlerEntityFlux(response, response.bodyToFlux(elementTypeRef))); } + @Override + public Mono>> toEntityFlux(BodyExtractor, ? super ClientHttpResponse> bodyExtractor) { + return this.responseMono.flatMap(response -> + handlerEntityFlux(response, response.body(bodyExtractor))); + } + @Override public Mono> toBodilessEntity() { return this.responseMono.flatMap(response -> diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index c548e0b48af0..2a14a4b62ece 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -40,8 +40,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.util.DefaultUriBuilderFactory; @@ -889,7 +891,7 @@ ResponseSpec onRawStatus(IntPredicate statusCodePredicate, Mono>> toEntityFlux(Class elementType); /** - * Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}. + * Variant of {@link #toEntityFlux(Class)} with a {@link ParameterizedTypeReference}. * @param elementTypeReference the type of element to decode the target Flux to * @param the body element type * @return the {@code ResponseEntity} @@ -897,6 +899,15 @@ ResponseSpec onRawStatus(IntPredicate statusCodePredicate, */ Mono>> toEntityFlux(ParameterizedTypeReference elementTypeReference); + /** + * Variant of {@link #toEntityFlux(Class)} with a {@link BodyExtractor}. + * @param bodyExtractor the {@code BodyExtractor} that reads from the response + * @param the body element type + * @return the {@code ResponseEntity} + * @since 5.3.2 + */ + Mono>> toEntityFlux(BodyExtractor, ? super ClientHttpResponse> bodyExtractor); + /** * Return a {@code ResponseEntity} without a body. For an error response * (status code of 4xx or 5xx), the {@code Mono} emits a diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index ebc6f823001a..49c18860b23c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -31,6 +31,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.reactivestreams.Publisher; +import org.springframework.web.reactive.function.BodyExtractors; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -459,6 +460,7 @@ public void onStatusHandlersApplyForToEntityMethods() { testStatusHandlerForToEntity(spec.toEntityList(new ParameterizedTypeReference() {})); testStatusHandlerForToEntity(spec.toEntityFlux(String.class)); testStatusHandlerForToEntity(spec.toEntityFlux(new ParameterizedTypeReference() {})); + testStatusHandlerForToEntity(spec.toEntityFlux(BodyExtractors.toFlux(String.class))); } private void testStatusHandlerForToEntity(Publisher responsePublisher) { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 4a940669d073..e25269d94619 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -41,6 +41,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.web.reactive.function.BodyExtractors; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; @@ -342,6 +343,39 @@ void retrieveJsonArrayAsResponseEntityFlux(ClientHttpConnector connector) { }); } + @ParameterizedWebClientTest + void retrieveJsonArrayAsResponseEntityFluxWithBodyExtractor(ClientHttpConnector connector) { + startServer(connector); + + String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]"; + prepareResponse(response -> response + .setHeader("Content-Type", "application/json").setBody(content)); + + ResponseEntity> entity = this.webClient.get() + .uri("/json").accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntityFlux(BodyExtractors.toFlux(Pojo.class)) + .block(Duration.ofSeconds(3)); + + assertThat(entity).isNotNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(entity.getHeaders().getContentLength()).isEqualTo(58); + + assertThat(entity.getBody()).isNotNull(); + StepVerifier.create(entity.getBody()) + .expectNext(new Pojo("foo1", "bar1")) + .expectNext(new Pojo("foo2", "bar2")) + .expectComplete() + .verify(Duration.ofSeconds(3)); + + expectRequestCount(1); + expectRequest(request -> { + assertThat(request.getPath()).isEqualTo("/json"); + assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json"); + }); + } + @Test // gh-24788 void retrieveJsonArrayAsBodilessEntityShouldReleasesConnection() { From c4d7e6ff46348a34c55e45b1d26726d3ef6cf259 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 19 Nov 2020 21:25:21 +0000 Subject: [PATCH 0055/1294] Fix checkstyle violations See gh-26117 --- .../web/reactive/function/client/DefaultWebClientTests.java | 2 +- .../web/reactive/function/client/WebClientIntegrationTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 49c18860b23c..37928d2fb1b7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -31,7 +31,6 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.reactivestreams.Publisher; -import org.springframework.web.reactive.function.BodyExtractors; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -41,6 +40,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ClientCodecConfigurer; +import org.springframework.web.reactive.function.BodyExtractors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index e25269d94619..e8a082a6b628 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -41,7 +41,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.web.reactive.function.BodyExtractors; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; @@ -65,6 +64,7 @@ import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector; import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.testfixture.xml.Pojo; import static org.assertj.core.api.Assertions.assertThat; From f1af8a6ae956f2523cb7f2f63710164babac6ddf Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 19 Nov 2020 21:41:39 +0000 Subject: [PATCH 0056/1294] Fix checkstyle violation --- .../RequestMappingHandlerAdapterIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java index 6c53d75cda91..29a75dcdbe93 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java @@ -16,7 +16,7 @@ package org.springframework.web.servlet.mvc.method.annotation; -import java.awt.*; +import java.awt.Color; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; From 45a922704736beb7160bf7fdca5bd236caf9212d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 20 Nov 2020 12:39:28 +0100 Subject: [PATCH 0057/1294] Remove TODO from AbstractAspectJAdvice Despite the code duplication, we will not delegate to AopUtils.invokeJoinpointUsingReflection() from AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(). The rationale is that the exception message in invokeAdviceMethodWithGivenArgs() provides additional context via the pointcut expression, and we would lose that additional context if we simply delegate to AopUtils.invokeJoinpointUsingReflection(). We could introduce an overloaded variant of invokeJoinpointUsingReflection() that accepts an additional argument to provide the additional context for the exception message, but we don't think that would be the best solution for this particular use case. In light of that, we are simply removing the TODO. Closes gh-26126 --- .../org/springframework/aop/aspectj/AbstractAspectJAdvice.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index ab118f59a517..8ef1bf148a17 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -631,7 +631,6 @@ protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable } try { ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); - // TODO AopUtils.invokeJoinpointUsingReflection return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); } catch (IllegalArgumentException ex) { From e6324bd578a2deb35cd4049dcffacf66b5ee3aef Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 19 Nov 2020 13:36:49 +0100 Subject: [PATCH 0058/1294] Using WebAsyncManager in WebMvc.fn This commit removed the existing code for dealing with asynchronous results, and replaced it with code using the WebAsyncManager. Closes gh-25931 --- .../WebMvcConfigurationSupport.java | 9 +- .../servlet/function/AsyncServerResponse.java | 235 ++++-------------- .../DefaultEntityResponseBuilder.java | 111 +++++---- .../web/servlet/function/ServerResponse.java | 29 ++- .../support/HandlerFunctionAdapter.java | 80 +++++- .../RequestMappingHandlerAdapter.java | 5 +- 6 files changed, 236 insertions(+), 233 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 535ec014daa1..a87b57d5a88d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -682,7 +682,14 @@ protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() { */ @Bean public HandlerFunctionAdapter handlerFunctionAdapter() { - return new HandlerFunctionAdapter(); + HandlerFunctionAdapter adapter = new HandlerFunctionAdapter(); + + AsyncSupportConfigurer configurer = new AsyncSupportConfigurer(); + configureAsyncSupport(configurer); + if (configurer.getTimeout() != null) { + adapter.setAsyncRequestTimeout(configurer.getTimeout()); + } + return adapter; } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java index f099a3699d30..279a9b165c33 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java @@ -17,19 +17,14 @@ package org.springframework.web.servlet.function; import java.io.IOException; +import java.time.Duration; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.CompletionException; import java.util.function.Function; -import javax.servlet.AsyncContext; -import javax.servlet.AsyncListener; -import javax.servlet.ServletContext; import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; @@ -42,6 +37,10 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; +import org.springframework.web.context.request.async.AsyncWebRequest; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.servlet.ModelAndView; /** @@ -59,9 +58,13 @@ final class AsyncServerResponse extends ErrorHandlingServerResponse { private final CompletableFuture futureResponse; + @Nullable + private final Duration timeout; + - private AsyncServerResponse(CompletableFuture futureResponse) { + private AsyncServerResponse(CompletableFuture futureResponse, @Nullable Duration timeout) { this.futureResponse = futureResponse; + this.timeout = timeout; } @Override @@ -96,44 +99,62 @@ private R delegate(Function function) { @Nullable @Override - public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) { + public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) + throws ServletException, IOException { - SharedAsyncContextHttpServletRequest sharedRequest = new SharedAsyncContextHttpServletRequest(request); - AsyncContext asyncContext = sharedRequest.startAsync(request, response); - this.futureResponse.whenComplete((futureResponse, futureThrowable) -> { - try { - if (futureResponse != null) { - ModelAndView mav = futureResponse.writeTo(sharedRequest, response, context); - Assert.state(mav == null, "Asynchronous, rendering ServerResponse implementations are not " + - "supported in WebMvc.fn. Please use WebFlux.fn instead."); - } - else if (futureThrowable != null) { - handleError(futureThrowable, request, response, context); - } - } - catch (Throwable throwable) { - try { - handleError(throwable, request, response, context); - } - catch (ServletException | IOException ex) { - logger.warn("Asynchronous execution resulted in exception", ex); + writeAsync(request, response, createDeferredResult()); + return null; + } + + static void writeAsync(HttpServletRequest request, HttpServletResponse response, DeferredResult deferredResult) + throws ServletException, IOException { + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); + asyncManager.setAsyncWebRequest(asyncWebRequest); + try { + asyncManager.startDeferredResultProcessing(deferredResult); + } + catch (IOException | ServletException ex) { + throw ex; + } + catch (Exception ex) { + throw new ServletException("Async processing failed", ex); + } + + } + + private DeferredResult createDeferredResult() { + DeferredResult result; + if (this.timeout != null) { + result = new DeferredResult<>(this.timeout.toMillis()); + } + else { + result = new DeferredResult<>(); + } + this.futureResponse.handle((value, ex) -> { + if (ex != null) { + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); } + result.setErrorResult(ex); } - finally { - asyncContext.complete(); + else { + result.setResult(value); } + return null; }); - return null; + return result; } @SuppressWarnings({"unchecked"}) - public static ServerResponse create(Object o) { + public static ServerResponse create(Object o, @Nullable Duration timeout) { Assert.notNull(o, "Argument to async must not be null"); if (o instanceof CompletableFuture) { CompletableFuture futureResponse = (CompletableFuture) o; - return new AsyncServerResponse(futureResponse); + return new AsyncServerResponse(futureResponse, timeout); } else if (reactiveStreamsPresent) { ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); @@ -144,7 +165,7 @@ else if (reactiveStreamsPresent) { if (futureAdapter != null) { CompletableFuture futureResponse = (CompletableFuture) futureAdapter.fromPublisher(publisher); - return new AsyncServerResponse(futureResponse); + return new AsyncServerResponse(futureResponse, timeout); } } } @@ -152,150 +173,4 @@ else if (reactiveStreamsPresent) { } - /** - * HttpServletRequestWrapper that shares its AsyncContext between this - * AsyncServerResponse class and other, subsequent ServerResponse - * implementations, keeping track of how many contexts where - * started with startAsync(). This way, we make sure that - * {@link AsyncContext#complete()} only completes for the response that - * finishes last, and is not closed prematurely. - */ - private static final class SharedAsyncContextHttpServletRequest extends HttpServletRequestWrapper { - - private final AsyncContext asyncContext; - - private final AtomicInteger startedContexts; - - public SharedAsyncContextHttpServletRequest(HttpServletRequest request) { - super(request); - this.asyncContext = request.startAsync(); - this.startedContexts = new AtomicInteger(0); - } - - private SharedAsyncContextHttpServletRequest(HttpServletRequest request, AsyncContext asyncContext, - AtomicInteger startedContexts) { - super(request); - this.asyncContext = asyncContext; - this.startedContexts = startedContexts; - } - - @Override - public AsyncContext startAsync() throws IllegalStateException { - this.startedContexts.incrementAndGet(); - return new SharedAsyncContext(this.asyncContext, this, this.asyncContext.getResponse(), - this.startedContexts); - } - - @Override - public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) - throws IllegalStateException { - this.startedContexts.incrementAndGet(); - SharedAsyncContextHttpServletRequest sharedRequest; - if (servletRequest instanceof SharedAsyncContextHttpServletRequest) { - sharedRequest = (SharedAsyncContextHttpServletRequest) servletRequest; - } - else { - sharedRequest = new SharedAsyncContextHttpServletRequest((HttpServletRequest) servletRequest, - this.asyncContext, this.startedContexts); - } - return new SharedAsyncContext(this.asyncContext, sharedRequest, servletResponse, this.startedContexts); - } - - @Override - public AsyncContext getAsyncContext() { - return new SharedAsyncContext(this.asyncContext, this, this.asyncContext.getResponse(), this.startedContexts); - } - - - private static final class SharedAsyncContext implements AsyncContext { - - private final AsyncContext delegate; - - private final AtomicInteger openContexts; - - private final ServletRequest request; - - private final ServletResponse response; - - - public SharedAsyncContext(AsyncContext delegate, SharedAsyncContextHttpServletRequest request, - ServletResponse response, AtomicInteger usageCount) { - - this.delegate = delegate; - this.request = request; - this.response = response; - this.openContexts = usageCount; - } - - @Override - public void complete() { - if (this.openContexts.decrementAndGet() == 0) { - this.delegate.complete(); - } - } - - @Override - public ServletRequest getRequest() { - return this.request; - } - - @Override - public ServletResponse getResponse() { - return this.response; - } - - @Override - public boolean hasOriginalRequestAndResponse() { - return this.delegate.hasOriginalRequestAndResponse(); - } - - @Override - public void dispatch() { - this.delegate.dispatch(); - } - - @Override - public void dispatch(String path) { - this.delegate.dispatch(path); - } - - @Override - public void dispatch(ServletContext context, String path) { - this.delegate.dispatch(context, path); - } - - @Override - public void start(Runnable run) { - this.delegate.start(run); - } - - @Override - public void addListener(AsyncListener listener) { - this.delegate.addListener(listener); - } - - @Override - public void addListener(AsyncListener listener, - ServletRequest servletRequest, - ServletResponse servletResponse) { - - this.delegate.addListener(listener, servletRequest, servletResponse); - } - - @Override - public T createListener(Class clazz) throws ServletException { - return this.delegate.createListener(clazz); - } - - @Override - public void setTimeout(long timeout) { - this.delegate.setTimeout(timeout); - } - - @Override - public long getTimeout() { - return this.delegate.getTimeout(); - } - } - } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index 868b2d6a7ae2..e27b0f48c1fa 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -25,11 +25,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; import java.util.stream.Collectors; -import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -61,6 +61,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.servlet.ModelAndView; /** @@ -358,32 +359,39 @@ public CompletionStageEntityResponse(int statusCode, HttpHeaders headers, } @Override - protected ModelAndView writeToInternal(HttpServletRequest servletRequest, - HttpServletResponse servletResponse, Context context) { + protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, + Context context) throws ServletException, IOException { - AsyncContext asyncContext = servletRequest.startAsync(servletRequest, servletResponse); - entity().whenComplete((entity, throwable) -> { - try { - if (entity != null) { + DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); + AsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); + return null; + } - tryWriteEntityWithMessageConverters(entity, - (HttpServletRequest) asyncContext.getRequest(), - (HttpServletResponse) asyncContext.getResponse(), - context); - } - else if (throwable != null) { - handleError(throwable, servletRequest, servletResponse, context); + private DeferredResult createDeferredResult(HttpServletRequest request, HttpServletResponse response, + Context context) { + + DeferredResult result = new DeferredResult<>(); + entity().handle((value, ex) -> { + if (ex != null) { + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); } + result.setErrorResult(ex); } - catch (ServletException | IOException ex) { - logger.warn("Asynchronous execution resulted in exception", ex); - } - finally { - asyncContext.complete(); + else { + try { + tryWriteEntityWithMessageConverters(value, request, response, context); + result.setResult(null); + } + catch (ServletException | IOException writeException) { + result.setErrorResult(writeException); + } } + return null; }); - return null; + return result; } + } @@ -399,35 +407,46 @@ public PublisherEntityResponse(int statusCode, HttpHeaders headers, } @Override - protected ModelAndView writeToInternal(HttpServletRequest servletRequest, - HttpServletResponse servletResponse, Context context) { + protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, + Context context) throws ServletException, IOException { + + DeferredResult deferredResult = new DeferredResult<>(); + AsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); - AsyncContext asyncContext = servletRequest.startAsync(servletRequest, - new NoContentLengthResponseWrapper(servletResponse)); - entity().subscribe(new ProducingSubscriber(asyncContext, context)); + entity().subscribe(new DeferredResultSubscriber(servletRequest, servletResponse, context, deferredResult)); return null; } - @SuppressWarnings("SubscriberImplementation") - private class ProducingSubscriber implements Subscriber { - private final AsyncContext asyncContext; + private class DeferredResultSubscriber implements Subscriber { + + private final HttpServletRequest servletRequest; + + private final HttpServletResponse servletResponse; private final Context context; + private final DeferredResult deferredResult; + @Nullable private Subscription subscription; - public ProducingSubscriber(AsyncContext asyncContext, Context context) { - this.asyncContext = asyncContext; + + public DeferredResultSubscriber(HttpServletRequest servletRequest, + HttpServletResponse servletResponse, Context context, + DeferredResult deferredResult) { + + this.servletRequest = servletRequest; + this.servletResponse = new NoContentLengthResponseWrapper(servletResponse); this.context = context; + this.deferredResult = deferredResult; } @Override public void onSubscribe(Subscription s) { if (this.subscription == null) { this.subscription = s; - this.subscription.request(Long.MAX_VALUE); + this.subscription.request(1); } else { s.cancel(); @@ -435,32 +454,34 @@ public void onSubscribe(Subscription s) { } @Override - public void onNext(T element) { - HttpServletRequest servletRequest = (HttpServletRequest) this.asyncContext.getRequest(); - HttpServletResponse servletResponse = (HttpServletResponse) this.asyncContext.getResponse(); + public void onNext(T t) { + Assert.state(this.subscription != null, "No subscription"); try { - tryWriteEntityWithMessageConverters(element, servletRequest, servletResponse, this.context); + tryWriteEntityWithMessageConverters(t, this.servletRequest, this.servletResponse, this.context); + this.servletResponse.getOutputStream().flush(); + this.subscription.request(1); } catch (ServletException | IOException ex) { - onError(ex); + this.subscription.cancel(); + this.deferredResult.setErrorResult(ex); } } @Override public void onError(Throwable t) { - try { - handleError(t, (HttpServletRequest) this.asyncContext.getRequest(), - (HttpServletResponse) this.asyncContext.getResponse(), this.context); - } - catch (ServletException | IOException ex) { - logger.warn("Asynchronous execution resulted in exception", ex); - } - this.asyncContext.complete(); + this.deferredResult.setErrorResult(t); } @Override public void onComplete() { - this.asyncContext.complete(); + try { + this.servletResponse.getOutputStream().flush(); + this.deferredResult.setResult(null); + } + catch (IOException ex) { + this.deferredResult.setErrorResult(ex); + } + } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java index 025317adb0ac..ad932104989b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.net.URI; +import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; import java.util.Collection; @@ -240,7 +241,33 @@ static BodyBuilder unprocessableEntity() { * @since 5.3 */ static ServerResponse async(Object asyncResponse) { - return AsyncServerResponse.create(asyncResponse); + return AsyncServerResponse.create(asyncResponse, null); + } + + /** + * Create a (built) response with the given asynchronous response. + * Parameter {@code asyncResponse} can be a + * {@link CompletableFuture CompletableFuture<ServerResponse>} or + * {@link Publisher Publisher<ServerResponse>} (or any + * asynchronous producer of a single {@code ServerResponse} that can be + * adapted via the {@link ReactiveAdapterRegistry}). + * + *

    This method can be used to set the response status code, headers, and + * body based on an asynchronous result. If only the body is asynchronous, + * {@link BodyBuilder#body(Object)} can be used instead. + * + *

    Note that + * {@linkplain RenderingResponse rendering responses}, as returned by + * {@link BodyBuilder#render}, are not supported as value + * for {@code asyncResponse}. Use WebFlux.fn for asynchronous rendering. + * @param asyncResponse a {@code CompletableFuture} or + * {@code Publisher} + * @param timeout maximum time period to wait for before timing out + * @return the asynchronous response + * @since 5.3.2 + */ + static ServerResponse async(Object asyncResponse, Duration timeout) { + return AsyncServerResponse.create(asyncResponse, timeout); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java index 56ddf6137ca1..20f206aa3e3f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapter.java @@ -18,13 +18,21 @@ import java.util.List; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.core.Ordered; +import org.springframework.core.log.LogFormatUtils; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.web.context.request.async.AsyncWebRequest; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.function.HandlerFunction; @@ -40,8 +48,12 @@ */ public class HandlerFunctionAdapter implements HandlerAdapter, Ordered { + private static final Log logger = LogFactory.getLog(HandlerFunctionAdapter.class); + private int order = Ordered.LOWEST_PRECEDENCE; + @Nullable + private Long asyncRequestTimeout; /** * Specify the order value for this HandlerAdapter bean. @@ -57,6 +69,19 @@ public int getOrder() { return this.order; } + /** + * Specify the amount of time, in milliseconds, before concurrent handling + * should time out. In Servlet 3, the timeout begins after the main request + * processing thread has exited and ends when the request is dispatched again + * for further processing of the concurrently produced result. + *

    If this value is not set, the default timeout of the underlying + * implementation is used. + * @param timeout the timeout value in milliseconds + */ + public void setAsyncRequestTimeout(long timeout) { + this.asyncRequestTimeout = timeout; + } + @Override public boolean supports(Object handler) { return handler instanceof HandlerFunction; @@ -68,14 +93,34 @@ public ModelAndView handle(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Object handler) throws Exception { - - HandlerFunction handlerFunction = (HandlerFunction) handler; + WebAsyncManager asyncManager = getWebAsyncManager(servletRequest, servletResponse); ServerRequest serverRequest = getServerRequest(servletRequest); - ServerResponse serverResponse = handlerFunction.handle(serverRequest); + ServerResponse serverResponse; + + if (asyncManager.hasConcurrentResult()) { + serverResponse = handleAsync(asyncManager); + } + else { + HandlerFunction handlerFunction = (HandlerFunction) handler; + serverResponse = handlerFunction.handle(serverRequest); + } + + if (serverResponse != null) { + return serverResponse.writeTo(servletRequest, servletResponse, new ServerRequestContext(serverRequest)); + } + else { + return null; + } + } - return serverResponse.writeTo(servletRequest, servletResponse, - new ServerRequestContext(serverRequest)); + private WebAsyncManager getWebAsyncManager(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { + AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(servletRequest, servletResponse); + asyncWebRequest.setTimeout(this.asyncRequestTimeout); + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(servletRequest); + asyncManager.setAsyncWebRequest(asyncWebRequest); + return asyncManager; } private ServerRequest getServerRequest(HttpServletRequest servletRequest) { @@ -86,6 +131,31 @@ private ServerRequest getServerRequest(HttpServletRequest servletRequest) { return serverRequest; } + @Nullable + private ServerResponse handleAsync(WebAsyncManager asyncManager) throws Exception { + Object result = asyncManager.getConcurrentResult(); + asyncManager.clearConcurrentResult(); + LogFormatUtils.traceDebug(logger, traceOn -> { + String formatted = LogFormatUtils.formatValue(result, !traceOn); + return "Resume with async result [" + formatted + "]"; + }); + if (result instanceof ServerResponse) { + return (ServerResponse) result; + } + else if (result instanceof Exception) { + throw (Exception) result; + } + else if (result instanceof Throwable) { + throw new ServletException("Async processing failed", (Throwable) result); + } + else if (result == null) { + return null; + } + else { + throw new IllegalArgumentException("Unknown result from WebAsyncManager: [" + result + "]"); + } + } + @Override public long getLastModified(HttpServletRequest request, Object handler) { return -1L; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 9e5ea44432ec..935e494491a8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -880,7 +880,10 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, asyncManager.registerCallableInterceptors(this.callableInterceptors); asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); - if (asyncManager.hasConcurrentResult()) { + if (asyncManager.hasConcurrentResult() && + asyncManager.getConcurrentResultContext().length > 0 && + asyncManager.getConcurrentResultContext()[0] instanceof ModelAndViewContainer) { + Object result = asyncManager.getConcurrentResult(); mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; asyncManager.clearConcurrentResult(); From 4fb5d59c644a03c094bd4ecba562eab750815722 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 20 Nov 2020 18:40:10 +0100 Subject: [PATCH 0059/1294] Declare resolvedCharset as transient (restoring serializability) Closes gh-26127 --- .../org/springframework/util/MimeType.java | 29 ++++++++++++++----- .../springframework/util/MimeTypeTests.java | 19 ++++++++---- .../org/springframework/http/MediaType.java | 6 ++-- .../springframework/http/MediaTypeTests.java | 14 +++++++-- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java index e431589085ea..de29040b07f6 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeType.java +++ b/spring-core/src/main/java/org/springframework/util/MimeType.java @@ -16,6 +16,8 @@ package org.springframework.util; +import java.io.IOException; +import java.io.ObjectInputStream; import java.io.Serializable; import java.nio.charset.Charset; import java.util.BitSet; @@ -104,7 +106,7 @@ public class MimeType implements Comparable, Serializable { private final Map parameters; @Nullable - private Charset resolvedCharset; + private transient Charset resolvedCharset; @Nullable private volatile String toStringValue; @@ -184,9 +186,9 @@ public MimeType(String type, String subtype, @Nullable Map param this.subtype = subtype.toLowerCase(Locale.ENGLISH); if (!CollectionUtils.isEmpty(parameters)) { Map map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ENGLISH); - parameters.forEach((attribute, value) -> { - checkParameters(attribute, value); - map.put(attribute, value); + parameters.forEach((parameter, value) -> { + checkParameters(parameter, value); + map.put(parameter, value); }); this.parameters = Collections.unmodifiableMap(map); } @@ -224,11 +226,11 @@ private void checkToken(String token) { } } - protected void checkParameters(String attribute, String value) { - Assert.hasLength(attribute, "'attribute' must not be empty"); + protected void checkParameters(String parameter, String value) { + Assert.hasLength(parameter, "'parameter' must not be empty"); Assert.hasLength(value, "'value' must not be empty"); - checkToken(attribute); - if (PARAM_CHARSET.equals(attribute)) { + checkToken(parameter); + if (PARAM_CHARSET.equals(parameter)) { if (this.resolvedCharset == null) { this.resolvedCharset = Charset.forName(unquote(value)); } @@ -591,6 +593,17 @@ public int compareTo(MimeType other) { return 0; } + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization, just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + String charsetName = getParameter(PARAM_CHARSET); + if (charsetName != null) { + this.resolvedCharset = Charset.forName(unquote(charsetName)); + } + } + /** * Parse the given String value into a {@code MimeType} object, diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java index 7eb86378a5d0..38c7c8a5bfca 100644 --- a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java +++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java @@ -26,6 +26,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.testfixture.io.SerializationTestUtils; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; @@ -267,13 +268,13 @@ void parseMimeTypeSingleQuotedParameterValue() { assertThat(mimeType.getParameter("attr")).isEqualTo("'v>alue'"); } - @Test // SPR-16630 + @Test // SPR-16630 void parseMimeTypeWithSpacesAroundEquals() { MimeType mimeType = MimeTypeUtils.parseMimeType("multipart/x-mixed-replace;boundary = --myboundary"); assertThat(mimeType.getParameter("boundary")).isEqualTo("--myboundary"); } - @Test // SPR-16630 + @Test // SPR-16630 void parseMimeTypeWithSpacesAroundEqualsAndQuotedValue() { MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain; foo = \" bar \" "); assertThat(mimeType.getParameter("foo")).isEqualTo("\" bar \""); @@ -303,14 +304,14 @@ void parseMimeTypes() { assertThat(mimeTypes.size()).as("Invalid amount of mime types").isEqualTo(0); } - @Test // gh-23241 + @Test // gh-23241 void parseMimeTypesWithTrailingComma() { List mimeTypes = MimeTypeUtils.parseMimeTypes("text/plain, text/html,"); assertThat(mimeTypes).as("No mime types returned").isNotNull(); assertThat(mimeTypes.size()).as("Incorrect number of mime types").isEqualTo(2); } - @Test // SPR-17459 + @Test // SPR-17459 void parseMimeTypesWithQuotedParameters() { testWithQuotedParameters("foo/bar;param=\",\""); testWithQuotedParameters("foo/bar;param=\"s,a,\""); @@ -332,7 +333,7 @@ void parseSubtypeSuffix() { assertThat(type.getSubtypeSuffix()).isEqualTo("json"); } - @Test // gh-25350 + @Test // gh-25350 void wildcardSubtypeCompatibleWithSuffix() { MimeType applicationStar = new MimeType("application", "*"); MimeType applicationVndJson = new MimeType("application", "vnd.something+json"); @@ -413,4 +414,12 @@ void equalsIsCaseInsensitiveForCharsets() { assertThat(m2.compareTo(m1)).isEqualTo(0); } + @Test // gh-26127 + void serialize() throws Exception { + MimeType original = new MimeType("text", "plain", StandardCharsets.UTF_8); + MimeType deserialized = SerializationTestUtils.serializeAndDeserialize(original); + assertThat(deserialized).isEqualTo(original); + assertThat(original).isEqualTo(deserialized); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index d338e356c776..f4af4c01a507 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -514,9 +514,9 @@ public MediaType(MimeType mimeType) { @Override - protected void checkParameters(String attribute, String value) { - super.checkParameters(attribute, value); - if (PARAM_QUALITY_FACTOR.equals(attribute)) { + protected void checkParameters(String parameter, String value) { + super.checkParameters(parameter, value); + if (PARAM_QUALITY_FACTOR.equals(parameter)) { value = unquote(value); double d = Double.parseDouble(value); Assert.isTrue(d >= 0D && d <= 1D, diff --git a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java index 8849ff7ba424..74e73bfa7126 100644 --- a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java +++ b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -16,6 +16,7 @@ package org.springframework.http; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -26,6 +27,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.testfixture.io.SerializationTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -160,7 +162,7 @@ public void parseMediaTypes() throws Exception { assertThat(mediaTypes.size()).as("Invalid amount of media types").isEqualTo(0); } - @Test // gh-23241 + @Test // gh-23241 public void parseMediaTypesWithTrailingComma() { List mediaTypes = MediaType.parseMediaTypes("text/plain, text/html, "); assertThat(mediaTypes).as("No media types returned").isNotNull(); @@ -460,4 +462,12 @@ public void isConcrete() { assertThat(new MediaType("text", "*").isConcrete()).as("text/* concrete").isFalse(); } + @Test // gh-26127 + void serialize() throws Exception { + MediaType original = new MediaType("text", "plain", StandardCharsets.UTF_8); + MediaType deserialized = SerializationTestUtils.serializeAndDeserialize(original); + assertThat(deserialized).isEqualTo(original); + assertThat(original).isEqualTo(deserialized); + } + } From 42e492ef2a810a33991be60b4cba7f8bbf2dc4cb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 20 Nov 2020 19:00:06 +0100 Subject: [PATCH 0060/1294] Polishing --- .../java/org/springframework/util/MimeTypeTests.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java index 38c7c8a5bfca..59bfad035805 100644 --- a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java +++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java @@ -343,8 +343,9 @@ void wildcardSubtypeCompatibleWithSuffix() { private void testWithQuotedParameters(String... mimeTypes) { String s = String.join(",", mimeTypes); List actual = MimeTypeUtils.parseMimeTypes(s); + assertThat(actual.size()).isEqualTo(mimeTypes.length); - for (int i=0; i < mimeTypes.length; i++) { + for (int i = 0; i < mimeTypes.length; i++) { assertThat(actual.get(i).toString()).isEqualTo(mimeTypes[i]); } } @@ -371,6 +372,7 @@ void compareTo() { List result = new ArrayList<>(expected); Random rnd = new Random(); + // shuffle & sort 10 times for (int i = 0; i < 10; i++) { Collections.shuffle(result, rnd); @@ -400,11 +402,7 @@ void compareToCaseSensitivity() { assertThat(m2.compareTo(m1) != 0).as("Invalid comparison result").isTrue(); } - /** - * SPR-13157 - * @since 4.2 - */ - @Test + @Test // SPR-13157 void equalsIsCaseInsensitiveForCharsets() { MimeType m1 = new MimeType("text", "plain", singletonMap("charset", "UTF-8")); MimeType m2 = new MimeType("text", "plain", singletonMap("charset", "utf-8")); From 642d47ef8d078ebf457e83ae3922e9dbaf1c1af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 23 Nov 2020 11:24:37 +0100 Subject: [PATCH 0061/1294] Upgrade to Kotlin 1.4.20 Closes gh-26132 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index c41de3ccf215..5b938d8dcb3a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false id 'io.spring.nohttp' version '0.0.5.RELEASE' - id 'org.jetbrains.kotlin.jvm' version '1.4.10' apply false + id 'org.jetbrains.kotlin.jvm' version '1.4.20' apply false id 'org.jetbrains.dokka' version '0.10.1' apply false id 'org.asciidoctor.jvm.convert' version '3.1.0' id 'org.asciidoctor.jvm.pdf' version '3.1.0' @@ -9,7 +9,7 @@ plugins { id "io.freefair.aspectj" version '5.1.1' apply false id "com.github.ben-manes.versions" version '0.28.0' id "me.champeau.gradle.jmh" version "0.5.0" apply false - id "org.jetbrains.kotlin.plugin.serialization" version "1.4.10" apply false + id "org.jetbrains.kotlin.plugin.serialization" version "1.4.20" apply false } ext { @@ -31,7 +31,7 @@ configure(allprojects) { project -> mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" mavenBom "io.rsocket:rsocket-bom:1.1.0" mavenBom "org.eclipse.jetty:jetty-bom:9.4.34.v20201102" - mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.10" + mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.20" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.1" mavenBom "org.junit:junit-bom:5.7.0" } From 7c47f554c02a506a8f69af2f7d664794b0df3479 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 23 Nov 2020 13:32:52 +0100 Subject: [PATCH 0062/1294] Remove unnecessary check in RequestMappingHandlerAdapter --- .../mvc/method/annotation/RequestMappingHandlerAdapter.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 935e494491a8..9e5ea44432ec 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -880,10 +880,7 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, asyncManager.registerCallableInterceptors(this.callableInterceptors); asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); - if (asyncManager.hasConcurrentResult() && - asyncManager.getConcurrentResultContext().length > 0 && - asyncManager.getConcurrentResultContext()[0] instanceof ModelAndViewContainer) { - + if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; asyncManager.clearConcurrentResult(); From 4ed645e7109f1bf9560761d9963f5716e6cfc378 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 23 Nov 2020 14:49:43 +0100 Subject: [PATCH 0063/1294] Fix broken links to XSD schemas in ref docs Closes gh-26129 --- src/docs/asciidoc/core/core-appendix.adoc | 3 ++- src/docs/asciidoc/languages/dynamic-languages.adoc | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/docs/asciidoc/core/core-appendix.adoc b/src/docs/asciidoc/core/core-appendix.adoc index ce31eb55131a..877b9b1bfbf0 100644 --- a/src/docs/asciidoc/core/core-appendix.adoc +++ b/src/docs/asciidoc/core/core-appendix.adoc @@ -666,7 +666,8 @@ integrate such parsers into the Spring IoC container. To facilitate authoring configuration files that use a schema-aware XML editor, Spring's extensible XML configuration mechanism is based on XML Schema. If you are not familiar with Spring's current XML configuration extensions that come with the standard -Spring distribution, you should first read the appendix entitled <>. +Spring distribution, you should first read the previous section on <>. + To create new XML configuration extensions: diff --git a/src/docs/asciidoc/languages/dynamic-languages.adoc b/src/docs/asciidoc/languages/dynamic-languages.adoc index afa1ea076c3a..58ef162a2014 100644 --- a/src/docs/asciidoc/languages/dynamic-languages.adoc +++ b/src/docs/asciidoc/languages/dynamic-languages.adoc @@ -92,7 +92,7 @@ container. Using the dynamic-language-backed beans with a plain `BeanFactory` implementation is supported, but you have to manage the plumbing of the Spring internals to do so. -For more information on schema-based configuration, see <>. ==== @@ -176,7 +176,7 @@ of your dynamic language source files. The final step in the list in the <> involves defining dynamic-language-backed bean definitions, one for each bean that you want to configure (this is no different from normal JavaBean configuration). However, -instead of specifying the fully qualified classname of the class that is to be +instead of specifying the fully qualified class name of the class that is to be instantiated and configured by the container, you can use the `` element to define the dynamic language-backed bean. @@ -848,7 +848,7 @@ The `lang` elements in Spring XML configuration deal with exposing objects that written in a dynamic language (such as Groovy or BeanShell) as beans in the Spring container. These elements (and the dynamic language support) are comprehensively covered in -<>. See that chapter +<>. See that section for full details on this support and the `lang` elements. To use the elements in the `lang` schema, you need to have the following preamble at the From 298cb22bfecdf8b2655403fac8c3cd60e1214b20 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 23 Nov 2020 17:24:57 +0100 Subject: [PATCH 0064/1294] Ensure that projects can be imported into Eclipse IDE Recently the Spring Framework projects could no longer be imported into Eclipse IDE without compilation errors in JMH sources. This commit addresses this issue by applying workarounds for bugs in Gradle and the JMH plugin for Gradle. Gradle bug: https://github.com/gradle/gradle/issues/14932 JMH plugin bug: https://github.com/melix/jmh-gradle-plugin/issues/157 The Gradle bug has already been fixed in Gradle 6.8 RC1; however, the issue for the JMH plugin bug seems not to have been triaged yet. Closes gh-26140 --- gradle/ide.gradle | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 554d4b3c5432..39eefb55c4c6 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -29,12 +29,11 @@ eclipse.classpath.file.whenMerged { classpath -> classpath.entries.removeAll { entry -> (entry.path =~ /(?!.*?repack.*\.jar).*?\/([^\/]+)\/build\/libs\/[^\/]+\.jar/) } } - // Use separate main/test outputs (prevents WTP from packaging test classes) eclipse.classpath.defaultOutputDir = file(project.name+"/bin/eclipse") eclipse.classpath.file.beforeMerged { classpath -> classpath.entries.findAll{ it instanceof SourceFolder }.each { - if(it.output.startsWith("bin/")) { + if (it.output.startsWith("bin/")) { it.output = null } } @@ -56,6 +55,23 @@ eclipse.classpath.file.whenMerged { classpath -> } } +// Ensure that test fixture dependencies are handled properly in Gradle 6.7. +// Bug fixed in Gradle 6.8: https://github.com/gradle/gradle/issues/14932 +eclipse.classpath.file.whenMerged { + entries.findAll { it instanceof ProjectDependency }.each { + it.entryAttributes.remove('without_test_code') + } +} + +// Ensure that JMH sources and resources are treated as test classpath entries +// so that they can see test dependencies such as JUnit Jupiter APIs. +// https://github.com/melix/jmh-gradle-plugin/issues/157 +eclipse.classpath.file.whenMerged { + entries.findAll { it.path =~ /src\/jmh\/(java|resources)/ }.each { + it.entryAttributes['test'] = 'true' + } +} + // Allow projects to be used as WTP modules eclipse.project.natures "org.eclipse.wst.common.project.facet.core.nature" From fd70a9e95d416f80175f7ec21705528af12ec8ad Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 23 Nov 2020 17:42:49 +0100 Subject: [PATCH 0065/1294] Fix comment See gh-26140 --- gradle/ide.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/ide.gradle b/gradle/ide.gradle index 39eefb55c4c6..9b3e13caaf18 100644 --- a/gradle/ide.gradle +++ b/gradle/ide.gradle @@ -64,7 +64,7 @@ eclipse.classpath.file.whenMerged { } // Ensure that JMH sources and resources are treated as test classpath entries -// so that they can see test dependencies such as JUnit Jupiter APIs. +// so that they can see test fixtures. // https://github.com/melix/jmh-gradle-plugin/issues/157 eclipse.classpath.file.whenMerged { entries.findAll { it.path =~ /src\/jmh\/(java|resources)/ }.each { From cc4a1af09182978dc32d333e44e0e8229a89bc3b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 23 Nov 2020 09:56:02 +0000 Subject: [PATCH 0066/1294] Ensure AsyncSupportConfigurer is created once Now that we have two adapters that need access to the configurer, we need to save it in a field like others. See gh-25931 --- .../WebMvcConfigurationSupport.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index a87b57d5a88d..ae40c3dc9ffe 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -253,6 +253,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv @Nullable private Map corsConfigurations; + @Nullable + private AsyncSupportConfigurer asyncSupportConfigurer; + /** * Set the Spring {@link ApplicationContext}, e.g. for resource loading. @@ -652,8 +655,7 @@ public RequestMappingHandlerAdapter requestMappingHandlerAdapter( adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice())); } - AsyncSupportConfigurer configurer = new AsyncSupportConfigurer(); - configureAsyncSupport(configurer); + AsyncSupportConfigurer configurer = getAsyncSupportConfigurer(); if (configurer.getTaskExecutor() != null) { adapter.setTaskExecutor(configurer.getTaskExecutor()); } @@ -684,8 +686,7 @@ protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() { public HandlerFunctionAdapter handlerFunctionAdapter() { HandlerFunctionAdapter adapter = new HandlerFunctionAdapter(); - AsyncSupportConfigurer configurer = new AsyncSupportConfigurer(); - configureAsyncSupport(configurer); + AsyncSupportConfigurer configurer = getAsyncSupportConfigurer(); if (configurer.getTimeout() != null) { adapter.setAsyncRequestTimeout(configurer.getTimeout()); } @@ -717,13 +718,6 @@ protected MessageCodesResolver getMessageCodesResolver() { return null; } - /** - * Override this method to configure asynchronous request processing options. - * @see AsyncSupportConfigurer - */ - protected void configureAsyncSupport(AsyncSupportConfigurer configurer) { - } - /** * Return a {@link FormattingConversionService} for use with annotated controllers. *

    See {@link #addFormatters} as an alternative to overriding this method. @@ -945,6 +939,26 @@ else if (kotlinSerializationJsonPresent) { } } + /** + * Callback for building the {@link AsyncSupportConfigurer}. + * Delegates to {@link #configureAsyncSupport(AsyncSupportConfigurer)}. + * @since 5.3.2 + */ + protected AsyncSupportConfigurer getAsyncSupportConfigurer() { + if (this.asyncSupportConfigurer == null) { + this.asyncSupportConfigurer = new AsyncSupportConfigurer(); + configureAsyncSupport(this.asyncSupportConfigurer); + } + return this.asyncSupportConfigurer; + } + + /** + * Override this method to configure asynchronous request processing options. + * @see AsyncSupportConfigurer + */ + protected void configureAsyncSupport(AsyncSupportConfigurer configurer) { + } + /** * Return an instance of {@link CompositeUriComponentsContributor} for use with * {@link org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder}. From d8dafbc49d66738a20e389336ed8359bb7ed2c95 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 23 Nov 2020 17:21:35 +0000 Subject: [PATCH 0067/1294] Add DEBUG log message in MetadataExtractor Closes gh-26130 --- .../messaging/rsocket/DefaultMetadataExtractor.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java index 6f9b8f38d4db..a715277a974b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java @@ -31,6 +31,8 @@ import io.rsocket.metadata.CompositeMetadata; import io.rsocket.metadata.RoutingMetadata; import io.rsocket.metadata.WellKnownMimeType; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; @@ -52,6 +54,9 @@ */ public class DefaultMetadataExtractor implements MetadataExtractor, MetadataExtractorRegistry { + private static final Log logger = LogFactory.getLog(DefaultMetadataExtractor.class); + + private final List> decoders; private final Map> registrations = new HashMap<>(); @@ -119,6 +124,10 @@ public Map extract(Payload payload, MimeType metadataMimeType) { else { extractEntry(payload.metadata().slice(), metadataMimeType.toString(), result); } + if (logger.isDebugEnabled()) { + logger.debug("Values extracted from metadata: " + result + + " with registrations for " + this.registrations.keySet() + "."); + } return result; } @@ -175,7 +184,7 @@ public void extract(ByteBuf content, Map result) { @Override public String toString() { - return "mimeType=" + this.mimeType + ", targetType=" + this.targetType; + return "\"" + this.mimeType + "\" => " + this.targetType; } } From 23006d417b854ad3c4596370917488a5044b00d4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 23 Nov 2020 21:46:14 +0000 Subject: [PATCH 0068/1294] Deprecate context method in WebClient See gh-25710 --- .../function/client/DefaultWebClient.java | 1 + .../reactive/function/client/WebClient.java | 10 +++++----- src/docs/asciidoc/web/webflux-webclient.adoc | 19 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index b68888d0e332..aebc7760ac0e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -306,6 +306,7 @@ public RequestBodySpec attributes(Consumer> attributesConsum } @Override + @SuppressWarnings("deprecation") public RequestBodySpec context(Function contextModifier) { this.contextModifier = (this.contextModifier != null ? this.contextModifier.andThen(contextModifier) : contextModifier); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 2a14a4b62ece..c43566e6319f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -474,14 +474,14 @@ interface RequestHeadersSpec> { S attributes(Consumer> attributesConsumer); /** - * Provide a function to populate the Reactor {@code Context}. In contrast - * to {@link #attribute(String, Object) attributes} which apply only to - * the current request, the Reactor {@code Context} transparently propagates - * to the downstream processing chain which may include other nested or - * successive calls over HTTP or via other reactive clients. + * Provide a function to populate the Reactor {@code Context}. * @param contextModifier the function to modify the context with + * @deprecated in 5.3.2 to be removed soon after; this method cannot + * provide context to downstream (nested or subsequent) requests and is + * of limited value. * @since 5.3.1 */ + @Deprecated S context(Function contextModifier); /** diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 124b21acfb24..8542368f8f4a 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -951,6 +951,11 @@ For example: .awaitBody() ---- +Note that you can configure a `defaultRequest` callback globally at the +`WebClient.Builder` level which lets you insert attributes into all requests, +which could be used for example in a Spring MVC application to populate +request attributes based on `ThreadLocal` data. + [[webflux-client-context]] == Context @@ -960,10 +965,8 @@ chain but they only influence the current request. If you want to pass informati propagates to additional requests that are nested, e.g. via `flatMap`, or executed after, e.g. via `concatMap`, then you'll need to use the Reactor `Context`. -`WebClient` exposes a method to populate the Reactor `Context` for a given request. -This information is available to filters for the current request and it also propagates -to subsequent requests or other reactive clients participating in the downstream -processing chain. For example: +The Reactor `Context` needs to be populated at the end of a reactive chain in order to +apply to all operations. For example: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -977,18 +980,14 @@ processing chain. For example: .build(); client.get().uri("https://example.org/") - .context(context -> context.put("foo", ...)) .retrieve() .bodyToMono(String.class) .flatMap(body -> { // perform nested request (context propagates automatically)... - }); + }) + .contextWrite(context -> context.put("foo", ...)); ---- -Note that you can also specify how to populate the context through the `defaultRequest` -method at the level of the `WebClient.Builder` and that applies to all requests. -This could be used for to example to pass information from `ThreadLocal` storage onto -a Reactor processing chain in a Spring MVC application. [[webflux-client-synchronous]] From 77d6f8bc000047b1607b4b6362cd07f063d71c30 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 23 Nov 2020 17:51:51 +0100 Subject: [PATCH 0069/1294] Polishing --- .../org/springframework/core/ReactiveAdapterRegistryTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java index 34b1907d1016..4dc08a704f29 100644 --- a/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java +++ b/spring-core/src/test/java/org/springframework/core/ReactiveAdapterRegistryTests.java @@ -367,8 +367,9 @@ private ReactiveAdapter getAdapter(Class reactiveType) { private static class ExtendedFlux extends Flux { @Override - public void subscribe(CoreSubscriber actual) { + public void subscribe(CoreSubscriber actual) { throw new UnsupportedOperationException(); } } + } From dea2029e94108f09e8b932efec2160d62d6dadb5 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 23 Nov 2020 16:43:41 +0100 Subject: [PATCH 0070/1294] Only write non-default charset in MultipartWriterSupport This commit only writes the 'charset' parameter in the written headers if it is non-default (not UTF-8), since RFC7578 states that the only allowed parameter is 'boundary'. Closes gh-25885 --- .../http/codec/multipart/MultipartHttpMessageWriter.java | 4 +++- .../http/codec/multipart/MultipartWriterSupport.java | 6 +++++- .../codec/multipart/MultipartHttpMessageWriterTests.java | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java index 62907ec3e22f..50e9a264d1a7 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java @@ -149,7 +149,9 @@ public HttpMessageWriter> getFormWriter() { /** * Set the character set to use for part headers such as * "Content-Disposition" (and its filename parameter). - *

    By default this is set to "UTF-8". + *

    By default this is set to "UTF-8". If changed from this default, + * the "Content-Type" header will have a "charset" parameter that specifies + * the character set used. */ public void setCharset(Charset charset) { Assert.notNull(charset, "Charset must not be null"); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java index d7cf5181219d..0ddae070e04b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java @@ -102,7 +102,11 @@ protected MediaType getMultipartMediaType(@Nullable MediaType mediaType, byte[] params.putAll(mediaType.getParameters()); } params.put("boundary", new String(boundary, StandardCharsets.US_ASCII)); - params.put("charset", getCharset().name()); + Charset charset = getCharset(); + if (!charset.equals(StandardCharsets.UTF_8) && + !charset.equals(StandardCharsets.US_ASCII) ) { + params.put("charset", getCharset().name()); + } mediaType = (mediaType != null ? mediaType : MediaType.MULTIPART_FORM_DATA); mediaType = new MediaType(mediaType, params); diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java index a412102f9c2c..167b300fd376 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java @@ -190,7 +190,7 @@ public void writeMultipartRelated() { assertThat(contentType.isCompatibleWith(mediaType)).isTrue(); assertThat(contentType.getParameter("type")).isEqualTo("foo"); assertThat(contentType.getParameter("boundary")).isNotEmpty(); - assertThat(contentType.getParameter("charset")).isEqualTo("UTF-8"); + assertThat(contentType.getParameter("charset")).isNull(); MultiValueMap requestParts = parse(this.response, hints); assertThat(requestParts.size()).isEqualTo(2); From 361299034403ee95baee3eaa1464ef1537552982 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 23 Nov 2020 16:42:18 +0100 Subject: [PATCH 0071/1294] Polishing --- .../multipart/MultipartHttpMessageWriter.java | 12 ------------ .../codec/multipart/MultipartWriterSupport.java | 17 +++++++++++++++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java index 50e9a264d1a7..d94104535d11 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java @@ -16,7 +16,6 @@ package org.springframework.http.codec.multipart; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -146,17 +145,6 @@ public HttpMessageWriter> getFormWriter() { return this.formWriter; } - /** - * Set the character set to use for part headers such as - * "Content-Disposition" (and its filename parameter). - *

    By default this is set to "UTF-8". If changed from this default, - * the "Content-Type" header will have a "charset" parameter that specifies - * the character set used. - */ - public void setCharset(Charset charset) { - Assert.notNull(charset, "Charset must not be null"); - this.charset = charset; - } @Override diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java index 0ddae070e04b..b07eb720a56a 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java @@ -30,6 +30,7 @@ import org.springframework.http.MediaType; import org.springframework.http.codec.LoggingCodecSupport; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.MimeTypeUtils; import org.springframework.util.MultiValueMap; @@ -44,9 +45,9 @@ public class MultipartWriterSupport extends LoggingCodecSupport { /** THe default charset used by the writer. */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - protected final List supportedMediaTypes; + private final List supportedMediaTypes; - protected Charset charset = DEFAULT_CHARSET; + private Charset charset = DEFAULT_CHARSET; /** @@ -64,6 +65,18 @@ public Charset getCharset() { return this.charset; } + /** + * Set the character set to use for part headers such as + * "Content-Disposition" (and its filename parameter). + *

    By default this is set to "UTF-8". If changed from this default, + * the "Content-Type" header will have a "charset" parameter that specifies + * the character set used. + */ + public void setCharset(Charset charset) { + Assert.notNull(charset, "Charset must not be null"); + this.charset = charset; + } + public List getWritableMediaTypes() { return this.supportedMediaTypes; } From 80701082cd872a2526823dc26bc7f9271e52bd48 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 24 Nov 2020 17:30:02 +0000 Subject: [PATCH 0072/1294] Set favorPathExtension to false by default Applies a change that was intended in #23915 but wasn't. Closes gh-26119 --- .../ContentNegotiationManagerFactoryBean.java | 2 +- ...entNegotiationManagerFactoryBeanTests.java | 5 +++-- .../web/servlet/config/MvcNamespaceTests.java | 14 ++++++++---- .../ContentNegotiationConfigurerTests.java | 21 +++++++++++++----- ...MvcConfigurationSupportExtensionTests.java | 22 ++++++------------- ...nnotationControllerHandlerMethodTests.java | 14 ++++++++++-- .../ContentNegotiatingViewResolverTests.java | 18 ++++++++++++--- ...mvc-config-content-negotiation-manager.xml | 1 + 8 files changed, 64 insertions(+), 33 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java index f0339ce18db1..c23db8e97c9c 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java @@ -108,7 +108,7 @@ public class ContentNegotiationManagerFactoryBean private String parameterName = "format"; - private boolean favorPathExtension = true; + private boolean favorPathExtension = false; private Map mediaTypes = new HashMap<>(); diff --git a/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java index ec85f08c995f..24628334e7be 100644 --- a/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java +++ b/spring-web/src/test/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBeanTests.java @@ -72,8 +72,8 @@ void defaultSettings() throws Exception { this.servletRequest.setRequestURI("/flower.gif"); assertThat(manager.resolveMediaTypes(this.webRequest)) - .as("Should be able to resolve file extensions by default") - .isEqualTo(Collections.singletonList(MediaType.IMAGE_GIF)); + .as("Should not resolve file extensions by default") + .containsExactly(MediaType.ALL); this.servletRequest.setRequestURI("/flower.foobarbaz"); @@ -226,6 +226,7 @@ void fileExtensions() { @Test void ignoreAcceptHeader() throws Exception { this.factoryBean.setIgnoreAcceptHeader(true); + this.factoryBean.setFavorParameter(true); this.factoryBean.afterPropertiesSet(); ContentNegotiationManager manager = this.factoryBean.getObject(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index 078abe58578c..1a1c4fcff38f 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -206,7 +206,9 @@ public void testDefaultConfig() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json"); NativeWebRequest webRequest = new ServletWebRequest(request); ContentNegotiationManager manager = mapping.getContentNegotiationManager(); - assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON)); + assertThat(manager.resolveMediaTypes(webRequest)) + .as("Should not resolve file extensions by default") + .containsExactly(MediaType.ALL); RequestMappingHandlerAdapter adapter = appContext.getBean(RequestMappingHandlerAdapter.class); assertThat(adapter).isNotNull(); @@ -743,13 +745,17 @@ public void testContentNegotiationManager() throws Exception { RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); ContentNegotiationManager manager = mapping.getContentNegotiationManager(); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.xml"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.setParameter("format", "xml"); NativeWebRequest webRequest = new ServletWebRequest(request); - assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(MediaType.valueOf("application/rss+xml"))); + assertThat(manager.resolveMediaTypes(webRequest)) + .containsExactly(MediaType.valueOf("application/rss+xml")); ViewResolverComposite compositeResolver = this.appContext.getBean(ViewResolverComposite.class); assertThat(compositeResolver).isNotNull(); - assertThat(compositeResolver.getViewResolvers().size()).as("Actual: " + compositeResolver.getViewResolvers()).isEqualTo(1); + assertThat(compositeResolver.getViewResolvers().size()) + .as("Actual: " + compositeResolver.getViewResolvers()) + .isEqualTo(1); ViewResolver resolver = compositeResolver.getViewResolvers().get(0); assertThat(resolver.getClass()).isEqualTo(ContentNegotiatingViewResolver.class); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurerTests.java index ea897d49fc3f..80c5f4195eec 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -59,26 +59,34 @@ public void defaultSettings() throws Exception { this.servletRequest.setRequestURI("/flower.gif"); - assertThat(manager.resolveMediaTypes(this.webRequest).get(0)).as("Should be able to resolve file extensions by default").isEqualTo(MediaType.IMAGE_GIF); + assertThat(manager.resolveMediaTypes(this.webRequest)) + .as("Should not resolve file extensions by default") + .containsExactly(MediaType.ALL); this.servletRequest.setRequestURI("/flower?format=gif"); this.servletRequest.addParameter("format", "gif"); - assertThat(manager.resolveMediaTypes(this.webRequest)).as("Should not resolve request parameters by default").isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST); + assertThat(manager.resolveMediaTypes(this.webRequest)) + .as("Should not resolve request parameters by default") + .isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST); this.servletRequest.setRequestURI("/flower"); this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE); - assertThat(manager.resolveMediaTypes(this.webRequest).get(0)).as("Should resolve Accept header by default").isEqualTo(MediaType.IMAGE_GIF); + assertThat(manager.resolveMediaTypes(this.webRequest)) + .as("Should resolve Accept header by default") + .containsExactly(MediaType.IMAGE_GIF); } @Test public void addMediaTypes() throws Exception { + this.configurer.favorParameter(true); this.configurer.mediaTypes(Collections.singletonMap("json", MediaType.APPLICATION_JSON)); ContentNegotiationManager manager = this.configurer.buildContentNegotiationManager(); - this.servletRequest.setRequestURI("/flower.json"); - assertThat(manager.resolveMediaTypes(this.webRequest).get(0)).isEqualTo(MediaType.APPLICATION_JSON); + this.servletRequest.setRequestURI("/flower"); + this.servletRequest.addParameter("format", "json"); + assertThat(manager.resolveMediaTypes(this.webRequest)).containsExactly(MediaType.APPLICATION_JSON); } @Test @@ -97,6 +105,7 @@ public void favorParameter() throws Exception { @Test public void ignoreAcceptHeader() throws Exception { this.configurer.ignoreAcceptHeader(true); + this.configurer.favorParameter(true); ContentNegotiationManager manager = this.configurer.buildContentNegotiationManager(); this.servletRequest.setRequestURI("/flower"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java index 8b69c9c88767..929cf231ea6a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java @@ -33,7 +33,6 @@ import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.format.FormatterRegistry; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; @@ -91,7 +90,6 @@ import static com.fasterxml.jackson.databind.MapperFeature.DEFAULT_VIEW_INCLUSION; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.springframework.http.MediaType.APPLICATION_ATOM_XML; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_XML; @@ -268,34 +266,28 @@ public void webBindingInitializer() throws Exception { @Test @SuppressWarnings("deprecation") public void contentNegotiation() throws Exception { - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); NativeWebRequest webRequest = new ServletWebRequest(request); RequestMappingHandlerMapping mapping = this.config.requestMappingHandlerMapping( this.config.mvcContentNegotiationManager(), this.config.mvcConversionService(), this.config.mvcResourceUrlProvider()); + + request.setParameter("f", "json"); ContentNegotiationManager manager = mapping.getContentNegotiationManager(); assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(APPLICATION_JSON)); - request.setRequestURI("/foo.xml"); + request.setParameter("f", "xml"); assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(APPLICATION_XML)); - request.setRequestURI("/foo.rss"); - assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(MediaType.valueOf("application/rss+xml"))); - - request.setRequestURI("/foo.atom"); - assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(APPLICATION_ATOM_XML)); - - request.setRequestURI("/foo"); - request.setParameter("f", "json"); - assertThat(manager.resolveMediaTypes(webRequest)).isEqualTo(Collections.singletonList(APPLICATION_JSON)); - - request.setRequestURI("/resources/foo.gif"); SimpleUrlHandlerMapping handlerMapping = (SimpleUrlHandlerMapping) this.config.resourceHandlerMapping( this.config.mvcContentNegotiationManager(), this.config.mvcConversionService(), this.config.mvcResourceUrlProvider()); handlerMapping.setApplicationContext(this.context); + + request = new MockHttpServletRequest("GET", "/resources/foo.gif"); HandlerExecutionChain chain = handlerMapping.getHandler(request); + assertThat(chain).isNotNull(); ResourceHttpRequestHandler handler = (ResourceHttpRequestHandler) chain.getHandler(); assertThat(handler).isNotNull(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index cadaa20ddfdd..0f962a958b50 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -1742,6 +1742,7 @@ void responseBodyAsHtml(boolean usePathPatterns) throws Exception { } ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean(); + factoryBean.setFavorPathExtension(true); factoryBean.afterPropertiesSet(); RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); @@ -1773,6 +1774,7 @@ void responseBodyAsHtml(boolean usePathPatterns) throws Exception { void responseBodyAsHtmlWithSuffixPresent(boolean usePathPatterns) throws Exception { initDispatcherServlet(TextRestController.class, usePathPatterns, wac -> { ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean(); + factoryBean.setFavorPathExtension(true); factoryBean.afterPropertiesSet(); RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("contentNegotiationManager", factoryBean.getObject()); @@ -1833,14 +1835,22 @@ void responseBodyAsHtmlWithProducesCondition(boolean usePathPatterns) throws Exc void responseBodyAsTextWithCssExtension(boolean usePathPatterns) throws Exception { initDispatcherServlet(TextRestController.class, usePathPatterns, wac -> { ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean(); + factoryBean.setFavorParameter(true); + factoryBean.addMediaType("css", MediaType.parseMediaType("text/css")); factoryBean.afterPropertiesSet(); + + RootBeanDefinition mappingDef = new RootBeanDefinition(RequestMappingHandlerMapping.class); + mappingDef.getPropertyValues().add("contentNegotiationManager", factoryBean.getObject()); + wac.registerBeanDefinition("handlerMapping", mappingDef); + RootBeanDefinition adapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); adapterDef.getPropertyValues().add("contentNegotiationManager", factoryBean.getObject()); wac.registerBeanDefinition("handlerAdapter", adapterDef); }); byte[] content = "body".getBytes(StandardCharsets.ISO_8859_1); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a4.css"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/a4"); + request.addParameter("format", "css"); request.setContent(content); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -3826,7 +3836,7 @@ public String a3(@RequestBody String body) throws IOException { return body; } - @RequestMapping(path = "/a4.css", method = RequestMethod.GET) + @RequestMapping(path = "/a4", method = RequestMethod.GET) public String a4(@RequestBody String body) { return body; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java index e3b8295dfb22..deef4171d28e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java @@ -34,6 +34,7 @@ import org.springframework.web.accept.HeaderContentNegotiationStrategy; import org.springframework.web.accept.MappingMediaTypeFileExtensionResolver; import org.springframework.web.accept.ParameterContentNegotiationStrategy; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.support.StaticWebApplicationContext; @@ -85,9 +86,16 @@ public void getMediaTypeAcceptHeaderWithProduces() throws Exception { @Test public void resolveViewNameWithPathExtension() throws Exception { - request.setRequestURI("/test.xls"); + request.setRequestURI("/test"); + request.setParameter("format", "xls"); + + String mediaType = "application/vnd.ms-excel"; + ContentNegotiationManager manager = new ContentNegotiationManager( + new ParameterContentNegotiationStrategy( + Collections.singletonMap("xls", MediaType.parseMediaType(mediaType)))); ViewResolver viewResolverMock = mock(ViewResolver.class); + viewResolver.setContentNegotiationManager(manager); viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); viewResolver.afterPropertiesSet(); @@ -98,7 +106,7 @@ public void resolveViewNameWithPathExtension() throws Exception { given(viewResolverMock.resolveViewName(viewName, locale)).willReturn(null); given(viewResolverMock.resolveViewName(viewName + ".xls", locale)).willReturn(viewMock); - given(viewMock.getContentType()).willReturn("application/vnd.ms-excel"); + given(viewMock.getContentType()).willReturn(mediaType); View result = viewResolver.resolveViewName(viewName, locale); assertThat(result).as("Invalid view").isSameAs(viewMock); @@ -307,8 +315,12 @@ public void resolveViewNameAcceptHeaderDefaultView() throws Exception { public void resolveViewNameFilename() throws Exception { request.setRequestURI("/test.html"); + ContentNegotiationManager manager = + new ContentNegotiationManager(new PathExtensionContentNegotiationStrategy()); + ViewResolver viewResolverMock1 = mock(ViewResolver.class, "viewResolver1"); ViewResolver viewResolverMock2 = mock(ViewResolver.class, "viewResolver2"); + viewResolver.setContentNegotiationManager(manager); viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2)); viewResolver.afterPropertiesSet(); @@ -336,7 +348,7 @@ public void resolveViewNameFilenameDefaultView() throws Exception { request.setRequestURI("/test.json"); Map mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON); - org.springframework.web.accept.PathExtensionContentNegotiationStrategy pathStrategy = new org.springframework.web.accept.PathExtensionContentNegotiationStrategy(mapping); + PathExtensionContentNegotiationStrategy pathStrategy = new PathExtensionContentNegotiationStrategy(mapping); viewResolver.setContentNegotiationManager(new ContentNegotiationManager(pathStrategy)); ViewResolver viewResolverMock1 = mock(ViewResolver.class); diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml index 3d2fc2d5eb10..62fe63a5c7c3 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-content-negotiation-manager.xml @@ -14,6 +14,7 @@ + xml=application/rss+xml From 86f9716fef89a25462f5d55c9d430cf8cd62c82c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 25 Nov 2020 11:35:06 +0100 Subject: [PATCH 0073/1294] Remove misleading default note on ISO.DATE_TIME Closes gh-26134 --- .../org/springframework/format/annotation/DateTimeFormat.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java index 3f3008b16ead..488e78d7da8e 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java @@ -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. @@ -106,7 +106,6 @@ enum ISO { /** * The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX}, * e.g. "2000-10-31T01:30:00.000-05:00". - *

    This is the default if no annotation value is specified. */ DATE_TIME, From 73e1f24ac17eb39a361ba90f0490f3e7eafe38ba Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 25 Nov 2020 11:36:12 +0100 Subject: [PATCH 0074/1294] Restore HttpHeaders-based constructor for binary compatibility Closes gh-26151 --- .../reactive/AbstractServerHttpRequest.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 527445c92eb7..3b8bb8ad7046 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -69,7 +69,8 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { * Constructor with the URI and headers for the request. * @param uri the URI for the request * @param contextPath the context path for the request - * @param headers the headers for the request + * @param headers the headers for the request (as {@link MultiValueMap}) + * @since 5.3 */ public AbstractServerHttpRequest(URI uri, @Nullable String contextPath, MultiValueMap headers) { this.uri = uri; @@ -77,6 +78,18 @@ public AbstractServerHttpRequest(URI uri, @Nullable String contextPath, MultiVal this.headers = HttpHeaders.readOnlyHttpHeaders(headers); } + /** + * Constructor with the URI and headers for the request. + * @param uri the URI for the request + * @param contextPath the context path for the request + * @param headers the headers for the request (as {@link HttpHeaders}) + */ + public AbstractServerHttpRequest(URI uri, @Nullable String contextPath, HttpHeaders headers) { + this.uri = uri; + this.path = RequestPath.parse(uri, contextPath); + this.headers = HttpHeaders.readOnlyHttpHeaders(headers); + } + @Override public String getId() { From 1f701d9badb624c847d92de47c8cc17cce2a4977 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 25 Nov 2020 11:41:06 +0100 Subject: [PATCH 0075/1294] Upgrade to Jetty 9.4.35 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5b938d8dcb3a..7d086598640b 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ configure(allprojects) { project -> mavenBom "io.projectreactor:reactor-bom:2020.0.1" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" mavenBom "io.rsocket:rsocket-bom:1.1.0" - mavenBom "org.eclipse.jetty:jetty-bom:9.4.34.v20201102" + mavenBom "org.eclipse.jetty:jetty-bom:9.4.35.v20201120" mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.20" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.1" mavenBom "org.junit:junit-bom:5.7.0" From 43faa439ab567c382e50ae670560024cebdf63d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 26 Nov 2020 12:15:23 +0100 Subject: [PATCH 0076/1294] Refine kotlinx.serialization support This commit introduces the following changes: - Converters/codecs are now used based on generic type info. - On WebMvc and WebFlux, kotlinx.serialization is enabled along to Jackson because it only serializes Kotlin @Serializable classes which is not enough for error or actuator endpoints in Boot as described on spring-projects/spring-boot#24238. TODO: leverage Kotlin/kotlinx.serialization#1164 when fixed. Closes gh-26147 --- ...tlinSerializationJsonMessageConverter.java | 1 + .../json/KotlinSerializationJsonDecoder.java | 31 ++++++++++++++++++- .../json/KotlinSerializationJsonEncoder.java | 11 +++++-- .../http/codec/support/BaseDefaultCodecs.java | 17 ++++++---- ...SerializationJsonHttpMessageConverter.java | 23 ++++++++++++++ .../support/ClientCodecConfigurerTests.java | 17 ++++++---- .../codec/support/CodecConfigurerTests.java | 14 ++++++--- .../support/ServerCodecConfigurerTests.java | 9 ++++-- .../KotlinSerializationJsonDecoderTests.kt | 5 +++ .../KotlinSerializationJsonEncoderTests.kt | 6 +++- ...ializationJsonHttpMessageConverterTests.kt | 21 +++++++++++++ .../WebMvcConfigurationSupport.java | 6 ++-- src/docs/asciidoc/languages/kotlin.adoc | 16 ++++------ 13 files changed, 142 insertions(+), 35 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java index 213e424edfec..475a1f50008c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java @@ -94,6 +94,7 @@ protected String toJson(Object payload, Type resolvedType) { * Tries to find a serializer that can marshall or unmarshall instances of the given type * using kotlinx.serialization. If no serializer can be found, an exception is thrown. *

    Resolved serializers are cached and cached results are returned on successive calls. + * TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed * @param type the type to find a serializer for * @return a resolved serializer for the given type * @throws RuntimeException if no serializer supporting the given type can be found diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java index 0d71367f7dc1..58f582bb1e88 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java @@ -69,10 +69,38 @@ public KotlinSerializationJsonDecoder(Json json) { this.json = json; } + /** + * Configure a limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. This can be a result of + * decoding to a single {@code DataBuffer}, + * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, + * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. + * It can also occur when splitting the input stream, e.g. delimited text, + * in which case the limit applies to data buffered between delimiters. + *

    By default this is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + */ + public void setMaxInMemorySize(int byteCount) { + this.stringDecoder.setMaxInMemorySize(byteCount); + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + */ + public int getMaxInMemorySize() { + return this.stringDecoder.getMaxInMemorySize(); + } + @Override public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { - return (super.canDecode(elementType, mimeType) && !CharSequence.class.isAssignableFrom(elementType.toClass())); + try { + serializer(elementType.getType()); + return (super.canDecode(elementType, mimeType) && !CharSequence.class.isAssignableFrom(elementType.toClass())); + } + catch (Exception ex) { + return false; + } } @Override @@ -95,6 +123,7 @@ public Mono decodeToMono(Publisher inputStream, ResolvableTy * Tries to find a serializer that can marshall or unmarshall instances of the given type * using kotlinx.serialization. If no serializer can be found, an exception is thrown. *

    Resolved serializers are cached and cached results are returned on successive calls. + * TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed * @param type the type to find a serializer for * @return a resolved serializer for the given type * @throws RuntimeException if no serializer supporting the given type can be found diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java index f2a619734c61..cd577d4c8280 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java @@ -71,8 +71,14 @@ public KotlinSerializationJsonEncoder(Json json) { @Override public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { - return (super.canEncode(elementType, mimeType) && !String.class.isAssignableFrom(elementType.toClass()) && - !ServerSentEvent.class.isAssignableFrom(elementType.toClass())); + try { + serializer(elementType.getType()); + return (super.canEncode(elementType, mimeType) && !String.class.isAssignableFrom(elementType.toClass()) && + !ServerSentEvent.class.isAssignableFrom(elementType.toClass())); + } + catch (Exception ex) { + return false; + } } @Override @@ -105,6 +111,7 @@ public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, * Tries to find a serializer that can marshall or unmarshall instances of the given type * using kotlinx.serialization. If no serializer can be found, an exception is thrown. *

    Resolved serializers are cached and cached results are returned on successive calls. + * TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed * @param type the type to find a serializer for * @return a resolved serializer for the given type * @throws RuntimeException if no serializer supporting the given type can be found diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java index 0f40420298e2..00bc8887fae8 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java @@ -312,6 +312,11 @@ private void initCodec(@Nullable Object codec) { ((ProtobufDecoder) codec).setMaxMessageSize(size); } } + if (kotlinSerializationJsonPresent) { + if (codec instanceof KotlinSerializationJsonDecoder) { + ((KotlinSerializationJsonDecoder) codec).setMaxInMemorySize(size); + } + } if (jackson2Present) { if (codec instanceof AbstractJackson2Decoder) { ((AbstractJackson2Decoder) codec).setMaxInMemorySize(size); @@ -385,12 +390,12 @@ final List> getObjectReaders() { return Collections.emptyList(); } List> readers = new ArrayList<>(); + if (kotlinSerializationJsonPresent) { + addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder())); + } if (jackson2Present) { addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder())); } - else if (kotlinSerializationJsonPresent) { - addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder())); - } if (jackson2SmilePresent) { addCodec(readers, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ? (Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder())); @@ -484,12 +489,12 @@ final List> getObjectWriters() { */ final List> getBaseObjectWriters() { List> writers = new ArrayList<>(); + if (kotlinSerializationJsonPresent) { + writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder())); + } if (jackson2Present) { writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder())); } - else if (kotlinSerializationJsonPresent) { - writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder())); - } if (jackson2SmilePresent) { writers.add(new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ? (Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder())); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java index 9190f21cd7ba..7fe280a770c0 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java @@ -88,6 +88,28 @@ protected boolean supports(Class clazz) { } } + @Override + public boolean canRead(Type type, @Nullable Class contextClass, @Nullable MediaType mediaType) { + try { + serializer(GenericTypeResolver.resolveType(type, contextClass)); + return canRead(mediaType); + } + catch (Exception ex) { + return false; + } + } + + @Override + public boolean canWrite(@Nullable Type type, @Nullable Class clazz, @Nullable MediaType mediaType) { + try { + serializer(GenericTypeResolver.resolveType(type, clazz)); + return canWrite(mediaType); + } + catch (Exception ex) { + return false; + } + } + @Override public final Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { @@ -151,6 +173,7 @@ private Charset getCharsetToUse(@Nullable MediaType contentType) { * Tries to find a serializer that can marshall or unmarshall instances of the given type * using kotlinx.serialization. If no serializer can be found, an exception is thrown. *

    Resolved serializers are cached and cached results are returned on successive calls. + * TODO Avoid relying on throwing exception when https://github.com/Kotlin/kotlinx.serialization/pull/1164 is fixed * @param type the type to find a serializer for * @return a resolved serializer for the given type * @throws RuntimeException if no serializer supporting the given type can be found diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java index 1c40633785a9..60bec60fb6b6 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java @@ -56,6 +56,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.json.KotlinSerializationJsonDecoder; +import org.springframework.http.codec.json.KotlinSerializationJsonEncoder; import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; @@ -81,7 +83,7 @@ public class ClientCodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(13); + assertThat(readers.size()).isEqualTo(14); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -91,6 +93,7 @@ public void defaultReaders() { assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class); // SPR-16804 assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -101,7 +104,7 @@ public void defaultReaders() { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(12); + assertThat(writers.size()).isEqualTo(13); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -110,6 +113,7 @@ public void defaultWriters() { assertStringEncoder(getNextEncoder(writers), true); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -130,7 +134,7 @@ public void maxInMemorySize() { int size = 99; this.configurer.defaultCodecs().maxInMemorySize(size); List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(13); + assertThat(readers.size()).isEqualTo(14); assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); @@ -140,6 +144,7 @@ public void maxInMemorySize() { assertThat(((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()).isEqualTo(size); assertThat(((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); @@ -187,7 +192,7 @@ public void clonedConfigurer() { writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); assertThat(sseDecoder).isNotSameAs(jackson2Decoder); - assertThat(writers).hasSize(11); + assertThat(writers).hasSize(12); } @Test // gh-24194 @@ -197,7 +202,7 @@ public void cloneShouldNotDropMultipartCodecs() { List> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); - assertThat(writers).hasSize(11); + assertThat(writers).hasSize(12); } @Test @@ -211,7 +216,7 @@ public void cloneShouldNotBeImpactedByChangesToOriginal() { List> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); - assertThat(writers).hasSize(11); + assertThat(writers).hasSize(12); } private Decoder getNextDecoder(List> readers) { diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java index 26cb40fd2212..003646099fdd 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java @@ -52,6 +52,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.json.KotlinSerializationJsonDecoder; +import org.springframework.http.codec.json.KotlinSerializationJsonEncoder; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; @@ -79,7 +81,7 @@ class CodecConfigurerTests { @Test void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(12); + assertThat(readers.size()).isEqualTo(13); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -88,6 +90,7 @@ void defaultReaders() { assertStringDecoder(getNextDecoder(readers), true); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -97,7 +100,7 @@ void defaultReaders() { @Test void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(11); + assertThat(writers.size()).isEqualTo(12); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -105,6 +108,7 @@ void defaultWriters() { assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ResourceHttpMessageWriter.class); assertStringEncoder(getNextEncoder(writers), true); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -133,7 +137,7 @@ void defaultAndCustomReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(16); + assertThat(readers.size()).isEqualTo(17); assertThat(getNextDecoder(readers)).isSameAs(customDecoder1); assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader1); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); @@ -146,6 +150,7 @@ void defaultAndCustomReaders() { assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); assertThat(getNextDecoder(readers)).isSameAs(customDecoder2); assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader2); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -174,7 +179,7 @@ void defaultAndCustomWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(15); + assertThat(writers.size()).isEqualTo(16); assertThat(getNextEncoder(writers)).isSameAs(customEncoder1); assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter1); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); @@ -186,6 +191,7 @@ void defaultAndCustomWriters() { assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(getNextEncoder(writers)).isSameAs(customEncoder2); assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter2); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java index 3e24238068ad..0e458925b03c 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java @@ -56,6 +56,8 @@ import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; +import org.springframework.http.codec.json.KotlinSerializationJsonDecoder; +import org.springframework.http.codec.json.KotlinSerializationJsonEncoder; import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.PartHttpMessageWriter; @@ -83,7 +85,7 @@ public class ServerCodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(14); + assertThat(readers.size()).isEqualTo(15); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -94,6 +96,7 @@ public void defaultReaders() { assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(DefaultPartHttpMessageReader.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -103,7 +106,7 @@ public void defaultReaders() { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(13); + assertThat(writers.size()).isEqualTo(14); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -112,6 +115,7 @@ public void defaultWriters() { assertStringEncoder(getNextEncoder(writers), true); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -152,6 +156,7 @@ public void maxInMemorySize() { DefaultPartHttpMessageReader reader = (DefaultPartHttpMessageReader) multipartReader.getPartReader(); assertThat((reader).getMaxInMemorySize()).isEqualTo(size); + assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt index 6be5d1981cd6..2dedc390e33e 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt @@ -53,6 +53,11 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), null, MediaType.APPLICATION_PDF)).isFalse() } @Test @@ -60,6 +68,11 @@ class KotlinSerializationJsonHttpMessageConverterTests { assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_JSON)).isTrue() assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_JSON)).isTrue() + + assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_PDF)).isFalse() } @Test @@ -296,4 +309,12 @@ class KotlinSerializationJsonHttpMessageConverterTests { ) data class NotSerializableBean(val string: String) + + open class TypeBase + + inline fun typeTokenOf(): Type { + val base = object : TypeBase() {} + val superType = base::class.java.genericSuperclass!! + return (superType as ParameterizedType).actualTypeArguments.first()!! + } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index ae40c3dc9ffe..4995b3f28ff4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -906,6 +906,9 @@ else if (jaxb2Present) { } } + if (kotlinSerializationJsonPresent) { + messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); + } if (jackson2Present) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); if (this.applicationContext != null) { @@ -919,9 +922,6 @@ else if (gsonPresent) { else if (jsonbPresent) { messageConverters.add(new JsonbHttpMessageConverter()); } - else if (kotlinSerializationJsonPresent) { - messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); - } if (jackson2SmilePresent) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile(); diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc index 76355eabf2f2..f5cdfe5bcb26 100644 --- a/src/docs/asciidoc/languages/kotlin.adoc +++ b/src/docs/asciidoc/languages/kotlin.adoc @@ -389,17 +389,13 @@ project for more details. === Kotlin multiplatform serialization As of Spring Framework 5.3, https://github.com/Kotlin/kotlinx.serialization[Kotlin multiplatform serialization] is -supported in Spring MVC, Spring WebFlux and Spring Messaging. The builtin support currently only targets JSON format. - -To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] and make sure neither -Jackson, GSON or JSONB are in the classpath. - -NOTE: For a typical Spring Boot web application, that can be achieved by excluding `spring-boot-starter-json` dependency. - -In Spring MVC, if you need Jackson, GSON or JSONB for other purposes, you can keep them on the classpath and -<> to remove `MappingJackson2HttpMessageConverter` and add -`KotlinSerializationJsonHttpMessageConverter`. +supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently only targets JSON format. +To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin. +With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since +Kotlin serialization is designed to serialize only Kotlin classes annotated with `@Serializable`. +With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration, +if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually. == Coroutines From 42216b77dfce376fd9c6843c63ad3be9a99f4b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=A6=D1=8B=D0=BF?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2?= Date: Thu, 26 Nov 2020 16:16:26 +0200 Subject: [PATCH 0077/1294] Remove unused package-private class o.s.w.u.p.SubSequence --- .../web/util/pattern/SubSequence.java | 64 ------------------- 1 file changed, 64 deletions(-) delete mode 100644 spring-web/src/main/java/org/springframework/web/util/pattern/SubSequence.java diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/SubSequence.java b/spring-web/src/main/java/org/springframework/web/util/pattern/SubSequence.java deleted file mode 100644 index f99c5ce082f7..000000000000 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/SubSequence.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2002-2017 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.util.pattern; - -/** - * Used to represent a subsection of an array, useful when wanting to pass that subset of data - * to another method (e.g. a java regex matcher) but not wanting to create a new string object - * to hold all that data. - * - * @author Andy Clement - * @since 5.0 - */ -class SubSequence implements CharSequence { - - private final char[] chars; - - private final int start; - - private final int end; - - - SubSequence(char[] chars, int start, int end) { - this.chars = chars; - this.start = start; - this.end = end; - } - - - @Override - public int length() { - return (this.end - this.start); - } - - @Override - public char charAt(int index) { - return this.chars[this.start + index]; - } - - @Override - public CharSequence subSequence(int start, int end) { - return new SubSequence(this.chars, this.start + start, this.start + end); - } - - - @Override - public String toString() { - return new String(this.chars, this.start, this.end - this.start); - } - -} From d46091565b0da437f6c9d501122e7f2ddaf1f79a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 Nov 2020 16:14:39 +0000 Subject: [PATCH 0078/1294] MessageHeaderAccessor handle self-copy correctly 1. Revert changes in setHeader from 5.2.9 that caused regression on self-copy. 2. Update copyHeaders to ensure it still gets a copy of native headers. 3. Exit if source and target are the same instance, as an optimization. Closes gh-26155 --- .../support/MessageHeaderAccessor.java | 26 ++++---- .../support/NativeMessageHeaderAccessor.java | 60 ++++++++++++------- .../NativeMessageHeaderAccessorTests.java | 10 ++++ 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java index ef7130f54a93..95e04b63f489 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java @@ -378,13 +378,14 @@ private List getMatchingHeaderNames(String pattern, @Nullable Map headersToCopy) { - if (headersToCopy != null) { - headersToCopy.forEach((key, value) -> { - if (!isReadOnly(key)) { - setHeader(key, value); - } - }); + if (headersToCopy == null || this.headers == headersToCopy) { + return; } + headersToCopy.forEach((key, value) -> { + if (!isReadOnly(key)) { + setHeader(key, value); + } + }); } /** @@ -392,13 +393,14 @@ public void copyHeaders(@Nullable Map headersToCopy) { *

    This operation will not overwrite any existing values. */ public void copyHeadersIfAbsent(@Nullable Map headersToCopy) { - if (headersToCopy != null) { - headersToCopy.forEach((key, value) -> { - if (!isReadOnly(key)) { - setHeaderIfAbsent(key, value); - } - }); + if (headersToCopy == null || this.headers == headersToCopy) { + return; } + headersToCopy.forEach((key, value) -> { + if (!isReadOnly(key)) { + setHeaderIfAbsent(key, value); + } + }); } protected boolean isReadOnly(String headerName) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java index 3aea04dbca0a..25fdb60fa739 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java @@ -75,6 +75,8 @@ protected NativeMessageHeaderAccessor(@Nullable Message message) { @SuppressWarnings("unchecked") Map> map = (Map>) getHeader(NATIVE_HEADERS); if (map != null) { + // setHeader checks for equality but we need copy of native headers + setHeader(NATIVE_HEADERS, null); setHeader(NATIVE_HEADERS, new LinkedMultiValueMap<>(map)); } } @@ -103,6 +105,8 @@ public void setImmutable() { if (isMutable()) { Map> map = getNativeHeaders(); if (map != null) { + // setHeader checks for equality but we need immutable wrapper + setHeader(NATIVE_HEADERS, null); setHeader(NATIVE_HEADERS, Collections.unmodifiableMap(map)); } super.setImmutable(); @@ -110,31 +114,19 @@ public void setImmutable() { } @Override - public void setHeader(String name, @Nullable Object value) { - if (name.equalsIgnoreCase(NATIVE_HEADERS)) { - // Force removal since setHeader checks for equality - super.setHeader(NATIVE_HEADERS, null); + public void copyHeaders(@Nullable Map headersToCopy) { + if (headersToCopy == null) { + return; } - super.setHeader(name, value); - } - @Override - @SuppressWarnings("unchecked") - public void copyHeaders(@Nullable Map headersToCopy) { - if (headersToCopy != null) { - Map> nativeHeaders = getNativeHeaders(); - Map> map = (Map>) headersToCopy.get(NATIVE_HEADERS); - if (map != null) { - if (nativeHeaders != null) { - nativeHeaders.putAll(map); - } - else { - nativeHeaders = new LinkedMultiValueMap<>(map); - } - } - super.copyHeaders(headersToCopy); - setHeader(NATIVE_HEADERS, nativeHeaders); + @SuppressWarnings("unchecked") + Map> map = (Map>) headersToCopy.get(NATIVE_HEADERS); + if (map != null && map != getNativeHeaders()) { + map.forEach(this::setNativeHeaderValues); } + + // setHeader checks for equality, native headers should be equal by now + super.copyHeaders(headersToCopy); } /** @@ -201,6 +193,30 @@ public void setNativeHeader(String name, @Nullable String value) { } } + /** + * Variant of {@link #addNativeHeader(String, String)} for all values. + * @since 5.2.12 + */ + public void setNativeHeaderValues(String name, @Nullable List values) { + Assert.state(isMutable(), "Already immutable"); + Map> map = getNativeHeaders(); + if (values == null) { + if (map != null && map.get(name) != null) { + setModified(true); + map.remove(name); + } + return; + } + if (map == null) { + map = new LinkedMultiValueMap<>(3); + setHeader(NATIVE_HEADERS, map); + } + if (!ObjectUtils.nullSafeEquals(values, getHeader(name))) { + setModified(true); + map.put(name, new ArrayList<>(values)); + } + } + /** * Add the specified native header value to existing values. *

    In order for this to work, the accessor must be {@link #isMutable() diff --git a/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java index 726f80c256e3..4d0e7b4b672a 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java @@ -241,4 +241,14 @@ void copyImmutableToMutable() { assertThat(((NativeMessageHeaderAccessor) accessor).getNativeHeader("foo")).containsExactly("bar", "baz"); } + @Test // gh-26155 + void copySelf() { + NativeMessageHeaderAccessor accessor = new NativeMessageHeaderAccessor(); + accessor.addNativeHeader("foo", "bar"); + accessor.setHeader("otherHeader", "otherHeaderValue"); + accessor.setLeaveMutable(true); + + // Does not fail with ConcurrentModificationException + accessor.copyHeaders(accessor.getMessageHeaders()); + } } From 11f6c4e7ab6b63f363aca63230aef111f476484e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 26 Nov 2020 21:51:25 +0000 Subject: [PATCH 0079/1294] Matching update for copyHeadersIfAbsent The change to copyHeaders from d46091 also applied to copyHeadersIfPresent. Closes gh-26155 --- .../support/NativeMessageHeaderAccessor.java | 15 +++++++++ .../NativeMessageHeaderAccessorTests.java | 33 ++++++++++++++----- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java index 25fdb60fa739..2a3607fde2b2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java @@ -129,6 +129,21 @@ public void copyHeaders(@Nullable Map headersToCopy) { super.copyHeaders(headersToCopy); } + @Override + public void copyHeadersIfAbsent(@Nullable Map headersToCopy) { + if (headersToCopy == null) { + return; + } + + @SuppressWarnings("unchecked") + Map> map = (Map>) headersToCopy.get(NATIVE_HEADERS); + if (map != null && getNativeHeaders() == null) { + map.forEach(this::setNativeHeaderValues); + } + + super.copyHeadersIfAbsent(headersToCopy); + } + /** * Whether the native header map contains the give header name. * @param headerName the name of the header diff --git a/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java index 4d0e7b4b672a..73eba06caa45 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java @@ -226,16 +226,33 @@ public void setImmutableIdempotent() { @Test // gh-25821 void copyImmutableToMutable() { - NativeMessageHeaderAccessor source = new NativeMessageHeaderAccessor(); - source.addNativeHeader("foo", "bar"); - Message message = MessageBuilder.createMessage("payload", source.getMessageHeaders()); + NativeMessageHeaderAccessor sourceAccessor = new NativeMessageHeaderAccessor(); + sourceAccessor.addNativeHeader("foo", "bar"); + Message source = MessageBuilder.createMessage("payload", sourceAccessor.getMessageHeaders()); - NativeMessageHeaderAccessor target = new NativeMessageHeaderAccessor(); - target.copyHeaders(message.getHeaders()); - target.setLeaveMutable(true); - message = MessageBuilder.createMessage(message.getPayload(), target.getMessageHeaders()); + NativeMessageHeaderAccessor targetAccessor = new NativeMessageHeaderAccessor(); + targetAccessor.copyHeaders(source.getHeaders()); + targetAccessor.setLeaveMutable(true); + Message target = MessageBuilder.createMessage(source.getPayload(), targetAccessor.getMessageHeaders()); - MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(message); + MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(target); + assertThat(accessor.isMutable()); + ((NativeMessageHeaderAccessor) accessor).addNativeHeader("foo", "baz"); + assertThat(((NativeMessageHeaderAccessor) accessor).getNativeHeader("foo")).containsExactly("bar", "baz"); + } + + @Test // gh-25821 + void copyIfAbsentImmutableToMutable() { + NativeMessageHeaderAccessor sourceAccessor = new NativeMessageHeaderAccessor(); + sourceAccessor.addNativeHeader("foo", "bar"); + Message source = MessageBuilder.createMessage("payload", sourceAccessor.getMessageHeaders()); + + MessageHeaderAccessor targetAccessor = new NativeMessageHeaderAccessor(); + targetAccessor.copyHeadersIfAbsent(source.getHeaders()); + targetAccessor.setLeaveMutable(true); + Message target = MessageBuilder.createMessage(source.getPayload(), targetAccessor.getMessageHeaders()); + + MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(target); assertThat(accessor.isMutable()); ((NativeMessageHeaderAccessor) accessor).addNativeHeader("foo", "baz"); assertThat(((NativeMessageHeaderAccessor) accessor).getNativeHeader("foo")).containsExactly("bar", "baz"); From 56dbe06d9b885951922fcfe98aa514b355e031c5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 27 Nov 2020 17:00:16 +0100 Subject: [PATCH 0080/1294] Register @Bean definitions as dependent on containing configuration class Closes gh-26167 --- .../beans/factory/support/ConstructorResolver.java | 1 + .../annotation/ConfigurationClassPostProcessorTests.java | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index f49690677a91..a6904735284b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -410,6 +410,7 @@ public BeanWrapper instantiateUsingFactoryMethod( if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) { throw new ImplicitlyAppearedSingletonException(); } + this.beanFactory.registerDependentBean(factoryBeanName, beanName); factoryClass = factoryBean.getClass(); isStatic = false; } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java index caf1063f1ad4..a25982a268c1 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -20,7 +20,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -107,7 +106,9 @@ public void enhancementIsPresentBecauseSingletonSemanticsAreRespected() { Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isSameAs(foo); - assertThat(Arrays.asList(beanFactory.getDependentBeans("foo")).contains("bar")).isTrue(); + assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("foo"), "bar")).isTrue(); + assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "foo")).isTrue(); + assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "bar")).isTrue(); } @Test @@ -119,7 +120,9 @@ public void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isSameAs(foo); - assertThat(Arrays.asList(beanFactory.getDependentBeans("foo")).contains("bar")).isTrue(); + assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("foo"), "bar")).isTrue(); + assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "foo")).isTrue(); + assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "bar")).isTrue(); } @Test From 622d0831a3f354112a062eac49d78784ce3731ad Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 27 Nov 2020 21:46:45 +0100 Subject: [PATCH 0081/1294] Polishing --- .../factory/support/ConstructorResolver.java | 11 ++++---- .../beans/factory/BeanFactoryUtilsTests.java | 27 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index a6904735284b..7647f11defa4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java @@ -67,6 +67,7 @@ /** * Delegate for resolving constructors and factory methods. + * *

    Performs constructor resolution through argument matching. * * @author Juergen Hoeller @@ -85,7 +86,7 @@ class ConstructorResolver { private static final Object[] EMPTY_ARGS = new Object[0]; /** - * Marker for autowired arguments in a cached argument array, to be later replaced + * Marker for autowired arguments in a cached argument array, to be replaced * by a {@linkplain #resolveAutowiredArgument resolved autowired argument}. */ private static final Object autowiredArgumentMarker = new Object(); @@ -149,7 +150,7 @@ public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, } } if (argsToResolve != null) { - argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true); + argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve); } } @@ -445,7 +446,7 @@ public BeanWrapper instantiateUsingFactoryMethod( } } if (argsToResolve != null) { - argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve, true); + argsToUse = resolvePreparedArguments(beanName, mbd, bw, factoryMethodToUse, argsToResolve); } } @@ -817,7 +818,7 @@ private ArgumentsHolder createArgumentArray( * Resolve the prepared arguments stored in the given bean definition. */ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw, - Executable executable, Object[] argsToResolve, boolean fallback) { + Executable executable, Object[] argsToResolve) { TypeConverter customConverter = this.beanFactory.getCustomTypeConverter(); TypeConverter converter = (customConverter != null ? customConverter : bw); @@ -830,7 +831,7 @@ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mb Object argValue = argsToResolve[argIndex]; MethodParameter methodParam = MethodParameter.forExecutable(executable, argIndex); if (argValue == autowiredArgumentMarker) { - argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, fallback); + argValue = resolveAutowiredArgument(methodParam, beanName, null, converter, true); } else if (argValue instanceof BeanMetadataElement) { argValue = valueResolver.resolveValueIfNecessary("constructor argument", argValue); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java index d70a7bbd1bd7..e869c9c6c906 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java @@ -63,9 +63,8 @@ public class BeanFactoryUtilsTests { @BeforeEach - public void setUp() { + public void setup() { // Interesting hierarchical factory to test counts. - // Slow to read so we cache it. DefaultListableBeanFactory grandParent = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(grandParent).loadBeanDefinitions(ROOT_CONTEXT); @@ -93,7 +92,7 @@ public void testHierarchicalCountBeansWithNonHierarchicalFactory() { * Check that override doesn't count as two separate beans. */ @Test - public void testHierarchicalCountBeansWithOverride() throws Exception { + public void testHierarchicalCountBeansWithOverride() { // Leaf count assertThat(this.listableBeanFactory.getBeanDefinitionCount() == 1).isTrue(); // Count minus duplicate @@ -101,14 +100,14 @@ public void testHierarchicalCountBeansWithOverride() throws Exception { } @Test - public void testHierarchicalNamesWithNoMatch() throws Exception { + public void testHierarchicalNamesWithNoMatch() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, NoOp.class)); assertThat(names.size()).isEqualTo(0); } @Test - public void testHierarchicalNamesWithMatchOnlyInRoot() throws Exception { + public void testHierarchicalNamesWithMatchOnlyInRoot() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, IndexedTestBean.class)); assertThat(names.size()).isEqualTo(1); @@ -118,7 +117,7 @@ public void testHierarchicalNamesWithMatchOnlyInRoot() throws Exception { } @Test - public void testGetBeanNamesForTypeWithOverride() throws Exception { + public void testGetBeanNamesForTypeWithOverride() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.listableBeanFactory, ITestBean.class)); // includes 2 TestBeans from FactoryBeans (DummyFactory definitions) @@ -236,7 +235,7 @@ public void testFindsBeansOfTypeWithDefaultFactory() { } @Test - public void testHierarchicalResolutionWithOverride() throws Exception { + public void testHierarchicalResolutionWithOverride() { Object test3 = this.listableBeanFactory.getBean("test3"); Object test = this.listableBeanFactory.getBean("test"); @@ -276,14 +275,14 @@ public void testHierarchicalResolutionWithOverride() throws Exception { } @Test - public void testHierarchicalNamesForAnnotationWithNoMatch() throws Exception { + public void testHierarchicalNamesForAnnotationWithNoMatch() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, Override.class)); assertThat(names.size()).isEqualTo(0); } @Test - public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() throws Exception { + public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() { List names = Arrays.asList( BeanFactoryUtils.beanNamesForAnnotationIncludingAncestors(this.listableBeanFactory, TestAnnotation.class)); assertThat(names.size()).isEqualTo(1); @@ -293,7 +292,7 @@ public void testHierarchicalNamesForAnnotationWithMatchOnlyInRoot() throws Excep } @Test - public void testGetBeanNamesForAnnotationWithOverride() throws Exception { + public void testGetBeanNamesForAnnotationWithOverride() { AnnotatedBean annotatedBean = new AnnotatedBean(); this.listableBeanFactory.registerSingleton("anotherAnnotatedBean", annotatedBean); List names = Arrays.asList( @@ -433,6 +432,7 @@ public void isSingletonAndIsPrototypeWithStaticFactory() { String basePackage() default ""; } + @Retention(RetentionPolicy.RUNTIME) @ControllerAdvice @interface RestControllerAdvice { @@ -444,18 +444,23 @@ public void isSingletonAndIsPrototypeWithStaticFactory() { String basePackage() default ""; } + @ControllerAdvice("com.example") static class ControllerAdviceClass { } + @RestControllerAdvice("com.example") static class RestControllerAdviceClass { } + static class TestBeanSmartFactoryBean implements SmartFactoryBean { private final TestBean testBean = new TestBean("enigma", 42); + private final boolean singleton; + private final boolean prototype; TestBeanSmartFactoryBean(boolean singleton, boolean prototype) { @@ -478,7 +483,7 @@ public Class getObjectType() { return TestBean.class; } - public TestBean getObject() throws Exception { + public TestBean getObject() { // We don't really care if the actual instance is a singleton or prototype // for the tests that use this factory. return this.testBean; From e9caee1b6e613185220be6296e1ede55965962e3 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 30 Nov 2020 07:21:51 +0100 Subject: [PATCH 0082/1294] Start building against Reactor 2020.0.2 snapshots See gh-26176 --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7d086598640b..79e9d8303692 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ configure(allprojects) { project -> imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.11.3" mavenBom "io.netty:netty-bom:4.1.54.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.1" + mavenBom "io.projectreactor:reactor-bom:2020.0.2-SNAPSHOT" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" mavenBom "io.rsocket:rsocket-bom:1.1.0" mavenBom "org.eclipse.jetty:jetty-bom:9.4.35.v20201120" @@ -291,6 +291,7 @@ configure(allprojects) { project -> repositories { mavenCentral() maven { url "https://repo.spring.io/libs-spring-framework-build" } + maven { url "https://repo.spring.io/snapshot" } // reactor } } configurations.all { From e7b0b65244213b9a6ec455176fc09082020bd024 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 30 Nov 2020 17:16:55 +0000 Subject: [PATCH 0083/1294] Support for MediaType mappings in ResourceWebHandler Closes gh-26170 --- .../config/ResourceHandlerRegistration.java | 30 ++++++++++- .../reactive/resource/ResourceWebHandler.java | 51 ++++++++++++++++++- .../config/ResourceHandlerRegistryTests.java | 12 +++++ .../resource/ResourceWebHandlerTests.java | 18 +++++++ .../web/reactive/resource/test/foo.bar | 1 + 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 spring-webflux/src/test/resources/org/springframework/web/reactive/resource/test/foo.bar diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java index 5686f6c8b557..45556ce4c0fc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -18,12 +18,16 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.springframework.cache.Cache; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; +import org.springframework.http.MediaTypeFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.reactive.resource.ResourceWebHandler; @@ -50,6 +54,10 @@ public class ResourceHandlerRegistration { private boolean useLastModified = true; + @Nullable + private Map mediaTypes; + + /** * Create a {@link ResourceHandlerRegistration} instance. @@ -146,6 +154,23 @@ public ResourceChainRegistration resourceChain(boolean cacheResources, Cache cac return this.resourceChainRegistration; } + /** + * Add mappings between file extensions extracted from the filename of static + * {@link Resource}s and the media types to use for the response. + *

    Use of this method is typically not necessary since mappings can be + * also determined via {@link MediaTypeFactory#getMediaType(Resource)}. + * @param mediaTypes media type mappings + * @since 5.3.2 + */ + public void setMediaTypes(Map mediaTypes) { + if (this.mediaTypes == null) { + this.mediaTypes = new HashMap<>(mediaTypes.size()); + } + this.mediaTypes.clear(); + this.mediaTypes.putAll(mediaTypes); + } + + /** * Returns the URL path patterns for the resource handler. */ @@ -168,6 +193,9 @@ protected ResourceWebHandler getRequestHandler() { handler.setCacheControl(this.cacheControl); } handler.setUseLastModified(this.useLastModified); + if (this.mediaTypes != null) { + handler.setMediaTypes(this.mediaTypes); + } return handler; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java index f51a9bff6a9f..8b58e163fbeb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java @@ -23,7 +23,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -111,6 +114,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean { @Nullable private ResourceHttpMessageWriter resourceHttpMessageWriter; + @Nullable + private Map mediaTypes; + @Nullable private ResourceLoader resourceLoader; @@ -230,6 +236,30 @@ public ResourceHttpMessageWriter getResourceHttpMessageWriter() { return this.resourceHttpMessageWriter; } + /** + * Add mappings between file extensions extracted from the filename of static + * {@link Resource}s and the media types to use for the response. + *

    Use of this method is typically not necessary since mappings can be + * also determined via {@link MediaTypeFactory#getMediaType(Resource)}. + * @param mediaTypes media type mappings + * @since 5.3.2 + */ + public void setMediaTypes(Map mediaTypes) { + if (this.mediaTypes == null) { + this.mediaTypes = new HashMap<>(mediaTypes.size()); + } + mediaTypes.forEach((ext, type) -> + this.mediaTypes.put(ext.toLowerCase(Locale.ENGLISH), type)); + } + + /** + * Return the {@link #setMediaTypes(Map) configured} media type mappings. + * @since 5.3.2 + */ + public Map getMediaTypes() { + return (this.mediaTypes != null ? this.mediaTypes : Collections.emptyMap()); + } + /** * Provide the ResourceLoader to load {@link #setLocationValues(List) * location values} with. @@ -374,7 +404,7 @@ public Mono handle(ServerWebExchange exchange) { } // Check the media type for the resource - MediaType mediaType = MediaTypeFactory.getMediaType(resource).orElse(null); + MediaType mediaType = getMediaType(resource); setHeaders(exchange, resource, mediaType); // Content phase @@ -535,6 +565,25 @@ protected boolean isInvalidPath(String path) { return false; } + @Nullable + private MediaType getMediaType(Resource resource) { + MediaType mediaType = null; + String filename = resource.getFilename(); + if (!CollectionUtils.isEmpty(this.mediaTypes)) { + String ext = StringUtils.getFilenameExtension(filename); + if (ext != null) { + mediaType = this.mediaTypes.get(ext.toLowerCase(Locale.ENGLISH)); + } + } + if (mediaType == null) { + List mediaTypes = MediaTypeFactory.getMediaTypes(filename); + if (!CollectionUtils.isEmpty(mediaTypes)) { + mediaType = mediaTypes.get(0); + } + } + return mediaType; + } + /** * Set headers on the response. Called for both GET and HEAD requests. * @param exchange current exchange diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java index 3401d6197247..490cc21a1b6c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java @@ -17,6 +17,7 @@ package org.springframework.web.reactive.config; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -28,6 +29,7 @@ import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.context.support.GenericApplicationContext; import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; import org.springframework.http.server.PathContainer; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; @@ -99,6 +101,16 @@ public void cacheControl() { .isEqualTo(CacheControl.noCache().cachePrivate().getHeaderValue()); } + @Test + public void mediaTypes() { + MediaType mediaType = MediaType.parseMediaType("foo/bar"); + this.registration.setMediaTypes(Collections.singletonMap("bar", mediaType)); + ResourceWebHandler requestHandler = this.registration.getRequestHandler(); + + assertThat(requestHandler.getMediaTypes()).size().isEqualTo(1); + assertThat(requestHandler.getMediaTypes()).containsEntry("bar", mediaType); + } + @Test public void order() { assertThat(this.registry.getHandlerMapping().getOrder()).isEqualTo(Integer.MAX_VALUE -1); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java index b96a7d4a284f..1232a9a60dfd 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java @@ -214,6 +214,24 @@ public void getResourceFromSubDirectoryOfAlternatePath() { assertResponseBody(exchange, "function foo() { console.log(\"hello world\"); }"); } + @Test + public void getResourceWithRegisteredMediaType() throws Exception { + MediaType mediaType = new MediaType("foo", "bar"); + + ResourceWebHandler handler = new ResourceWebHandler(); + handler.setLocations(Collections.singletonList(new ClassPathResource("test/", getClass()))); + handler.setMediaTypes(Collections.singletonMap("bar", mediaType)); + handler.afterPropertiesSet(); + + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("")); + setPathWithinHandlerMapping(exchange, "foo.bar"); + handler.handle(exchange).block(TIMEOUT); + + HttpHeaders headers = exchange.getResponse().getHeaders(); + assertThat(headers.getContentType()).isEqualTo(mediaType); + assertResponseBody(exchange, "foo bar foo bar foo bar"); + } + @Test // SPR-14577 public void getMediaTypeWithFavorPathExtensionOff() throws Exception { List paths = Collections.singletonList(new ClassPathResource("test/", getClass())); diff --git a/spring-webflux/src/test/resources/org/springframework/web/reactive/resource/test/foo.bar b/spring-webflux/src/test/resources/org/springframework/web/reactive/resource/test/foo.bar new file mode 100644 index 000000000000..83afc78bf089 --- /dev/null +++ b/spring-webflux/src/test/resources/org/springframework/web/reactive/resource/test/foo.bar @@ -0,0 +1 @@ +foo bar foo bar foo bar \ No newline at end of file From 0c825621b8d4a23bc819a72e786d7801d98a46b4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 30 Nov 2020 17:30:45 +0000 Subject: [PATCH 0084/1294] Avoid use of Optional wrapper to get List See gh-26170 --- .../org/springframework/http/MediaTypeFactory.java | 12 +++++++----- .../servlet/resource/ResourceHttpRequestHandler.java | 5 ++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/MediaTypeFactory.java b/spring-web/src/main/java/org/springframework/http/MediaTypeFactory.java index 7ad0969f2b9f..8487ab5b8868 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaTypeFactory.java +++ b/spring-web/src/main/java/org/springframework/http/MediaTypeFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -112,10 +112,12 @@ public static Optional getMediaType(@Nullable String filename) { * @return the corresponding media types, or an empty list if none found */ public static List getMediaTypes(@Nullable String filename) { - return Optional.ofNullable(StringUtils.getFilenameExtension(filename)) - .map(s -> s.toLowerCase(Locale.ENGLISH)) - .map(fileExtensionToMediaTypes::get) - .orElse(Collections.emptyList()); + List mediaTypes = null; + String ext = StringUtils.getFilenameExtension(filename); + if (ext != null) { + mediaTypes = fileExtensionToMediaTypes.get(ext.toLowerCase(Locale.ENGLISH)); + } + return (mediaTypes != null ? mediaTypes : Collections.emptyList()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index 0cd981b6dea1..03d5ca02502b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -736,7 +736,10 @@ protected MediaType getMediaType(HttpServletRequest request, Resource resource) mediaType = this.mediaTypes.get(ext.toLowerCase(Locale.ENGLISH)); } if (mediaType == null) { - mediaType = MediaTypeFactory.getMediaType(filename).orElse(null); + List mediaTypes = MediaTypeFactory.getMediaTypes(filename); + if (!CollectionUtils.isEmpty(mediaTypes)) { + mediaType = mediaTypes.get(0); + } } if (mediaType != null) { result = mediaType; From 0978cc629abe80fbb0999b9408b15dca1349f3c3 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 1 Dec 2020 11:19:10 +0100 Subject: [PATCH 0085/1294] Polishing --- .../web/servlet/function/DefaultServerRequest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java index f0fba03886b0..da9bd4b62e86 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java @@ -270,6 +270,10 @@ public Optional principal() { return Optional.ofNullable(this.serverHttpRequest.getPrincipal()); } + @Override + public String toString() { + return String.format("HTTP %s %s", method(), path()); + } static Optional checkNotModified( HttpServletRequest servletRequest, @Nullable Instant lastModified, @Nullable String etag) { From 69aed83504c098027e5d878df0b0cf40439facb5 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 1 Dec 2020 11:24:45 +0100 Subject: [PATCH 0086/1294] Use nested path variables instead of Servlet's Remove the overriden pathVariable method from the nested request wrapper,so that the merged attributes are used instead of the ones provided by servlet. Closes gh-26163 --- .../web/servlet/function/RequestPredicates.java | 5 ----- .../servlet/function/RouterFunctionsTests.java | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java index a83a40d618e4..664d6a851dc1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java @@ -1040,11 +1040,6 @@ public MultiValueMap multipartData() throws IOException, ServletEx return this.request.multipartData(); } - @Override - public String pathVariable(String name) { - return this.request.pathVariable(name); - } - @Override @SuppressWarnings("unchecked") public Map pathVariables() { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionsTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionsTests.java index f46a3770448b..787e2e709926 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionsTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionsTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.web.servlet.handler.PathPatternsTestUtils; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -97,4 +98,20 @@ public void nestNoMatch() { assertThat(resultHandlerFunction.isPresent()).isFalse(); } + @Test + public void nestPathVariable() { + HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); + RequestPredicate requestPredicate = request -> request.pathVariable("foo").equals("bar"); + RouterFunction nestedFunction = RouterFunctions.route(requestPredicate, handlerFunction); + + RouterFunction result = RouterFunctions.nest(RequestPredicates.path("/{foo}"), nestedFunction); + assertThat(result).isNotNull(); + + MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/bar"); + ServerRequest request = new DefaultServerRequest(servletRequest, Collections.emptyList()); + Optional> resultHandlerFunction = result.route(request); + assertThat(resultHandlerFunction.isPresent()).isTrue(); + assertThat(resultHandlerFunction.get()).isEqualTo(handlerFunction); + } + } From 1e19e51c9c7912c53726bbc250eb8efd3526aac4 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 1 Dec 2020 16:44:20 +0000 Subject: [PATCH 0087/1294] Fix exchangeToMono sample in reference Closes gh-26189 --- src/docs/asciidoc/web/webflux-webclient.adoc | 56 ++++++++++---------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 8542368f8f4a..4beaeab43807 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -547,37 +547,39 @@ depending on the response status: .Java ---- Mono entityMono = client.get() - .uri("/persons/1") - .accept(MediaType.APPLICATION_JSON) - .exchangeToMono(response -> { - if (response.statusCode().equals(HttpStatus.OK)) { - return response.bodyToMono(Person.class); - } - else if (response.statusCode().is4xxClientError()) { - return response.bodyToMono(ErrorContainer.class); - } - else { - return Mono.error(response.createException()); - } - }); + .uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchangeToMono(response -> { + if (response.statusCode().equals(HttpStatus.OK)) { + return response.bodyToMono(Person.class); + } + else if (response.statusCode().is4xxClientError()) { + // Suppress error status code + return response.bodyToMono(ErrorContainer.class); + } + else { + // Turn to error + return response.createException().flatMap(Mono::error); + } + }); ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - val entity = client.get() - .uri("/persons/1") - .accept(MediaType.APPLICATION_JSON) - .awaitExchange { - if (response.statusCode() == HttpStatus.OK) { - return response.awaitBody(); - } - else if (response.statusCode().is4xxClientError) { - return response.awaitBody(); - } - else { - return response.createExceptionAndAwait(); - } - } +val entity = client.get() + .uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange { + if (response.statusCode() == HttpStatus.OK) { + return response.awaitBody() + } + else if (response.statusCode().is4xxClientError) { + return response.awaitBody() + } + else { + throw response.createExceptionAndAwait() + } + } ---- When using the above, after the returned `Mono` or `Flux` completes, the response body From 5328184f3a22ed3d848f6004c50c721597e4d564 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 1 Dec 2020 17:44:57 +0000 Subject: [PATCH 0088/1294] ContentCachingResponseWrapper skips contentLength for chunked responses Closes gh-26182 --- .../util/ContentCachingResponseWrapper.java | 5 +- .../ContentCachingResponseWrapperTests.java | 69 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java diff --git a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java index c0d820ba0a0c..16198930494a 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java +++ b/spring-web/src/main/java/org/springframework/web/util/ContentCachingResponseWrapper.java @@ -27,6 +27,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; +import org.springframework.http.HttpHeaders; import org.springframework.lang.Nullable; import org.springframework.util.FastByteArrayOutputStream; @@ -209,7 +210,9 @@ protected void copyBodyToResponse(boolean complete) throws IOException { if (this.content.size() > 0) { HttpServletResponse rawResponse = (HttpServletResponse) getResponse(); if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) { - rawResponse.setContentLength(complete ? this.content.size() : this.contentLength); + if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) { + rawResponse.setContentLength(complete ? this.content.size() : this.contentLength); + } this.contentLength = null; } this.content.writeTo(rawResponse.getOutputStream()); diff --git a/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java new file mode 100644 index 000000000000..576d0287d17b --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java @@ -0,0 +1,69 @@ +/* + * 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.filter; + +import java.nio.charset.StandardCharsets; + +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.testfixture.servlet.MockHttpServletResponse; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ContentCachingResponseWrapper}. + * @author Rossen Stoyanchev + */ +public class ContentCachingResponseWrapperTests { + + @Test + void copyBodyToResponse() throws Exception { + byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + MockHttpServletResponse response = new MockHttpServletResponse(); + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + responseWrapper.setStatus(HttpServletResponse.SC_OK); + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + responseWrapper.copyBodyToResponse(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContentLength() > 0).isTrue(); + assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + } + + @Test + void copyBodyToResponseWithTransferEncoding() throws Exception { + byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(StandardCharsets.UTF_8); + MockHttpServletResponse response = new MockHttpServletResponse(); + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + responseWrapper.setStatus(HttpServletResponse.SC_OK); + responseWrapper.setHeader(HttpHeaders.TRANSFER_ENCODING, "chunked"); + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + responseWrapper.copyBodyToResponse(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getHeader(HttpHeaders.TRANSFER_ENCODING)).isEqualTo("chunked"); + assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isNull(); + assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + } + +} From 396fb0cd513588863491929ddbfe22a49de05beb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 2 Dec 2020 12:25:37 +0100 Subject: [PATCH 0089/1294] Support for multi-threaded addConverter calls Closes gh-26183 --- .../convert/support/GenericConversionService.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index f64ee4bcbac9..1134483e88a8 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -17,17 +17,17 @@ package org.springframework.core.convert.support; import java.lang.reflect.Array; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.CopyOnWriteArraySet; import org.springframework.core.DecoratingProxy; import org.springframework.core.ResolvableType; @@ -500,9 +500,9 @@ public int compareTo(ConverterCacheKey other) { */ private static class Converters { - private final Set globalConverters = new LinkedHashSet<>(); + private final Set globalConverters = new CopyOnWriteArraySet<>(); - private final Map converters = new LinkedHashMap<>(36); + private final Map converters = new ConcurrentHashMap<>(256); public void add(GenericConverter converter) { Set convertibleTypes = converter.getConvertibleTypes(); @@ -513,8 +513,7 @@ public void add(GenericConverter converter) { } else { for (ConvertiblePair convertiblePair : convertibleTypes) { - ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair); - convertersForPair.add(converter); + getMatchableConverters(convertiblePair).add(converter); } } } @@ -652,7 +651,7 @@ private List getConverterStrings() { */ private static class ConvertersForPair { - private final Deque converters = new ArrayDeque<>(1); + private final Deque converters = new ConcurrentLinkedDeque<>(); public void add(GenericConverter converter) { this.converters.addFirst(converter); From c7f2f50c1581487589874eed702ee557fa4f84be Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Wed, 2 Dec 2020 23:07:15 +0900 Subject: [PATCH 0090/1294] Polish ConfigurationClassPostProcessorTests Closes gh-26197 --- .../ConfigurationClassPostProcessorTests.java | 203 +++++++++--------- 1 file changed, 101 insertions(+), 102 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java index a25982a268c1..3b1f1a7f0dd9 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -65,7 +65,6 @@ import org.springframework.core.task.SyncTaskExecutor; import org.springframework.stereotype.Component; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -76,13 +75,13 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class ConfigurationClassPostProcessorTests { +class ConfigurationClassPostProcessorTests { private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @BeforeEach - public void setup() { + void setup() { QualifierAnnotationAutowireCandidateResolver acr = new QualifierAnnotationAutowireCandidateResolver(); acr.setBeanFactory(this.beanFactory); this.beanFactory.setAutowireCandidateResolver(acr); @@ -98,7 +97,7 @@ public void setup() { * We test for such a case below, and in doing so prove that enhancement is working. */ @Test - public void enhancementIsPresentBecauseSingletonSemanticsAreRespected() { + void enhancementIsPresentBecauseSingletonSemanticsAreRespected() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -106,13 +105,13 @@ public void enhancementIsPresentBecauseSingletonSemanticsAreRespected() { Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isSameAs(foo); - assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("foo"), "bar")).isTrue(); - assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "foo")).isTrue(); - assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "bar")).isTrue(); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + assertThat(beanFactory.getDependentBeans("config")).contains("foo"); + assertThat(beanFactory.getDependentBeans("config")).contains("bar"); } @Test - public void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { + void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class.getName())); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -120,13 +119,13 @@ public void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() Foo foo = beanFactory.getBean("foo", Foo.class); Bar bar = beanFactory.getBean("bar", Bar.class); assertThat(bar.foo).isSameAs(foo); - assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("foo"), "bar")).isTrue(); - assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "foo")).isTrue(); - assertThat(ObjectUtils.containsElement(beanFactory.getDependentBeans("config"), "bar")).isTrue(); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + assertThat(beanFactory.getDependentBeans("config")).contains("foo"); + assertThat(beanFactory.getDependentBeans("config")).contains("bar"); } @Test - public void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { + void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -137,7 +136,7 @@ public void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { } @Test - public void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalseUsingAsm() { + void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalseUsingAsm() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class.getName())); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -148,7 +147,7 @@ public void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalseUsingAsm() { } @Test - public void enhancementIsNotPresentForStaticMethods() { + void enhancementIsNotPresentForStaticMethods() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(StaticSingletonBeanConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -161,7 +160,7 @@ public void enhancementIsNotPresentForStaticMethods() { } @Test - public void enhancementIsNotPresentForStaticMethodsUsingAsm() { + void enhancementIsNotPresentForStaticMethodsUsingAsm() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(StaticSingletonBeanConfig.class.getName())); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -174,7 +173,7 @@ public void enhancementIsNotPresentForStaticMethodsUsingAsm() { } @Test - public void configurationIntrospectionOfInnerClassesWorksWithDotNameSyntax() { + void configurationIntrospectionOfInnerClassesWorksWithDotNameSyntax() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(getClass().getName() + ".SingletonBeanConfig")); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -188,7 +187,7 @@ public void configurationIntrospectionOfInnerClassesWorksWithDotNameSyntax() { * if a bean class is already loaded. */ @Test - public void alreadyLoadedConfigurationClasses() { + void alreadyLoadedConfigurationClasses() { beanFactory.registerBeanDefinition("unloadedConfig", new RootBeanDefinition(UnloadedConfig.class.getName())); beanFactory.registerBeanDefinition("loadedConfig", new RootBeanDefinition(LoadedConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); @@ -201,7 +200,7 @@ public void alreadyLoadedConfigurationClasses() { * Tests whether a bean definition without a specified bean class is handled correctly. */ @Test - public void postProcessorIntrospectsInheritedDefinitionsCorrectly() { + void postProcessorIntrospectsInheritedDefinitionsCorrectly() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); beanFactory.registerBeanDefinition("parent", new RootBeanDefinition(TestBean.class)); beanFactory.registerBeanDefinition("child", new ChildBeanDefinition("parent")); @@ -213,96 +212,96 @@ public void postProcessorIntrospectsInheritedDefinitionsCorrectly() { } @Test - public void postProcessorWorksWithComposedConfigurationUsingReflection() { + void postProcessorWorksWithComposedConfigurationUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition(ComposedConfigurationClass.class); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithComposedConfigurationUsingAsm() { + void postProcessorWorksWithComposedConfigurationUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition(ComposedConfigurationClass.class.getName()); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForBasePackageUsingReflection() { + void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForBasePackageUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedConfigurationWithAttributeOverrideForBasePackage.class); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForBasePackageUsingAsm() { + void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForBasePackageUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedConfigurationWithAttributeOverrideForBasePackage.class.getName()); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForExcludeFilterUsingReflection() { + void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForExcludeFilterUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedConfigurationWithAttributeOverrideForExcludeFilter.class); assertSupportForComposedAnnotationWithExclude(beanDefinition); } @Test - public void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForExcludeFilterUsingAsm() { + void postProcessorWorksWithComposedConfigurationWithAttributeOverrideForExcludeFilterUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedConfigurationWithAttributeOverrideForExcludeFilter.class.getName()); assertSupportForComposedAnnotationWithExclude(beanDefinition); } @Test - public void postProcessorWorksWithExtendedConfigurationWithAttributeOverrideForExcludesFilterUsingReflection() { + void postProcessorWorksWithExtendedConfigurationWithAttributeOverrideForExcludesFilterUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ExtendedConfigurationWithAttributeOverrideForExcludeFilter.class); assertSupportForComposedAnnotationWithExclude(beanDefinition); } @Test - public void postProcessorWorksWithExtendedConfigurationWithAttributeOverrideForExcludesFilterUsingAsm() { + void postProcessorWorksWithExtendedConfigurationWithAttributeOverrideForExcludesFilterUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ExtendedConfigurationWithAttributeOverrideForExcludeFilter.class.getName()); assertSupportForComposedAnnotationWithExclude(beanDefinition); } @Test - public void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverridesUsingReflection() { + void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverridesUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedComposedConfigurationWithAttributeOverridesClass.class); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverridesUsingAsm() { + void postProcessorWorksWithComposedComposedConfigurationWithAttributeOverridesUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( ComposedComposedConfigurationWithAttributeOverridesClass.class.getName()); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesUsingReflection() { + void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( MetaComponentScanConfigurationWithAttributeOverridesClass.class); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesUsingAsm() { + void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( MetaComponentScanConfigurationWithAttributeOverridesClass.class.getName()); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclassUsingReflection() { + void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclassUsingReflection() { RootBeanDefinition beanDefinition = new RootBeanDefinition( SubMetaComponentScanConfigurationWithAttributeOverridesClass.class); assertSupportForComposedAnnotation(beanDefinition); } @Test - public void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclassUsingAsm() { + void postProcessorWorksWithMetaComponentScanConfigurationWithAttributeOverridesSubclassUsingAsm() { RootBeanDefinition beanDefinition = new RootBeanDefinition( SubMetaComponentScanConfigurationWithAttributeOverridesClass.class.getName()); assertSupportForComposedAnnotation(beanDefinition); @@ -327,7 +326,7 @@ private void assertSupportForComposedAnnotationWithExclude(RootBeanDefinition be } @Test - public void postProcessorOverridesNonApplicationBeanDefinitions() { + void postProcessorOverridesNonApplicationBeanDefinitions() { RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); rbd.setRole(RootBeanDefinition.ROLE_SUPPORT); beanFactory.registerBeanDefinition("bar", rbd); @@ -340,7 +339,7 @@ public void postProcessorOverridesNonApplicationBeanDefinitions() { } @Test - public void postProcessorDoesNotOverrideRegularBeanDefinitions() { + void postProcessorDoesNotOverrideRegularBeanDefinitions() { RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); rbd.setResource(new DescriptiveResource("XML or something")); beanFactory.registerBeanDefinition("bar", rbd); @@ -352,7 +351,7 @@ public void postProcessorDoesNotOverrideRegularBeanDefinitions() { } @Test - public void postProcessorDoesNotOverrideRegularBeanDefinitionsEvenWithScopedProxy() { + void postProcessorDoesNotOverrideRegularBeanDefinitionsEvenWithScopedProxy() { RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); rbd.setResource(new DescriptiveResource("XML or something")); BeanDefinitionHolder proxied = ScopedProxyUtils.createScopedProxy( @@ -366,7 +365,7 @@ public void postProcessorDoesNotOverrideRegularBeanDefinitionsEvenWithScopedProx } @Test - public void postProcessorFailsOnImplicitOverrideIfOverridingIsNotAllowed() { + void postProcessorFailsOnImplicitOverrideIfOverridingIsNotAllowed() { RootBeanDefinition rbd = new RootBeanDefinition(TestBean.class); rbd.setResource(new DescriptiveResource("XML or something")); beanFactory.registerBeanDefinition("bar", rbd); @@ -381,7 +380,7 @@ public void postProcessorFailsOnImplicitOverrideIfOverridingIsNotAllowed() { } @Test // gh-25430 - public void detectAliasOverride() { + void detectAliasOverride() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(); beanFactory.setAllowBeanDefinitionOverriding(false); @@ -393,7 +392,7 @@ public void detectAliasOverride() { } @Test - public void configurationClassesProcessedInCorrectOrder() { + void configurationClassesProcessedInCorrectOrder() { beanFactory.registerBeanDefinition("config1", new RootBeanDefinition(OverridingSingletonBeanConfig.class)); beanFactory.registerBeanDefinition("config2", new RootBeanDefinition(SingletonBeanConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); @@ -407,7 +406,7 @@ public void configurationClassesProcessedInCorrectOrder() { } @Test - public void configurationClassesWithValidOverridingForProgrammaticCall() { + void configurationClassesWithValidOverridingForProgrammaticCall() { beanFactory.registerBeanDefinition("config1", new RootBeanDefinition(OverridingAgainSingletonBeanConfig.class)); beanFactory.registerBeanDefinition("config2", new RootBeanDefinition(OverridingSingletonBeanConfig.class)); beanFactory.registerBeanDefinition("config3", new RootBeanDefinition(SingletonBeanConfig.class)); @@ -422,7 +421,7 @@ public void configurationClassesWithValidOverridingForProgrammaticCall() { } @Test - public void configurationClassesWithInvalidOverridingForProgrammaticCall() { + void configurationClassesWithInvalidOverridingForProgrammaticCall() { beanFactory.registerBeanDefinition("config1", new RootBeanDefinition(InvalidOverridingSingletonBeanConfig.class)); beanFactory.registerBeanDefinition("config2", new RootBeanDefinition(OverridingSingletonBeanConfig.class)); beanFactory.registerBeanDefinition("config3", new RootBeanDefinition(SingletonBeanConfig.class)); @@ -438,7 +437,7 @@ public void configurationClassesWithInvalidOverridingForProgrammaticCall() { } @Test // SPR-15384 - public void nestedConfigurationClassesProcessedInCorrectOrder() { + void nestedConfigurationClassesProcessedInCorrectOrder() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(ConfigWithOrderedNestedClasses.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -451,7 +450,7 @@ public void nestedConfigurationClassesProcessedInCorrectOrder() { } @Test // SPR-16734 - public void innerConfigurationClassesProcessedInCorrectOrder() { + void innerConfigurationClassesProcessedInCorrectOrder() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(ConfigWithOrderedInnerClasses.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -465,7 +464,7 @@ public void innerConfigurationClassesProcessedInCorrectOrder() { } @Test - public void scopedProxyTargetMarkedAsNonAutowireCandidate() { + void scopedProxyTargetMarkedAsNonAutowireCandidate() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -482,7 +481,7 @@ public void scopedProxyTargetMarkedAsNonAutowireCandidate() { } @Test - public void processingAllowedOnlyOncePerProcessorRegistryPair() { + void processingAllowedOnlyOncePerProcessorRegistryPair() { DefaultListableBeanFactory bf1 = new DefaultListableBeanFactory(); DefaultListableBeanFactory bf2 = new DefaultListableBeanFactory(); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); @@ -495,7 +494,7 @@ public void processingAllowedOnlyOncePerProcessorRegistryPair() { } @Test - public void genericsBasedInjection() { + void genericsBasedInjection() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -512,7 +511,7 @@ public void genericsBasedInjection() { } @Test - public void genericsBasedInjectionWithScoped() { + void genericsBasedInjectionWithScoped() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -529,7 +528,7 @@ public void genericsBasedInjectionWithScoped() { } @Test - public void genericsBasedInjectionWithScopedProxy() { + void genericsBasedInjectionWithScopedProxy() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -549,7 +548,7 @@ public void genericsBasedInjectionWithScopedProxy() { } @Test - public void genericsBasedInjectionWithScopedProxyUsingAsm() { + void genericsBasedInjectionWithScopedProxyUsingAsm() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -569,7 +568,7 @@ public void genericsBasedInjectionWithScopedProxyUsingAsm() { } @Test - public void genericsBasedInjectionWithImplTypeAtInjectionPoint() { + void genericsBasedInjectionWithImplTypeAtInjectionPoint() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -586,7 +585,7 @@ public void genericsBasedInjectionWithImplTypeAtInjectionPoint() { } @Test - public void genericsBasedInjectionWithFactoryBean() { + void genericsBasedInjectionWithFactoryBean() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -605,7 +604,7 @@ public void genericsBasedInjectionWithFactoryBean() { } @Test - public void genericsBasedInjectionWithRawMatch() { + void genericsBasedInjectionWithRawMatch() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawMatchingConfiguration.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -614,7 +613,7 @@ public void genericsBasedInjectionWithRawMatch() { } @Test - public void genericsBasedInjectionWithWildcardMatch() { + void genericsBasedInjectionWithWildcardMatch() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(WildcardMatchingConfiguration.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); @@ -623,7 +622,7 @@ public void genericsBasedInjectionWithWildcardMatch() { } @Test - public void genericsBasedInjectionWithWildcardWithExtendsMatch() { + void genericsBasedInjectionWithWildcardWithExtendsMatch() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(WildcardWithExtendsConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); @@ -631,7 +630,7 @@ public void genericsBasedInjectionWithWildcardWithExtendsMatch() { } @Test - public void genericsBasedInjectionWithWildcardWithGenericExtendsMatch() { + void genericsBasedInjectionWithWildcardWithGenericExtendsMatch() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(WildcardWithGenericExtendsConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); @@ -639,12 +638,12 @@ public void genericsBasedInjectionWithWildcardWithGenericExtendsMatch() { } @Test - public void genericsBasedInjectionWithEarlyGenericsMatching() { + void genericsBasedInjectionWithEarlyGenericsMatching() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -656,13 +655,13 @@ public void genericsBasedInjectionWithEarlyGenericsMatching() { } @Test - public void genericsBasedInjectionWithLateGenericsMatching() { + void genericsBasedInjectionWithLateGenericsMatching() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -674,12 +673,12 @@ public void genericsBasedInjectionWithLateGenericsMatching() { } @Test - public void genericsBasedInjectionWithEarlyGenericsMatchingAndRawFactoryMethod() { + void genericsBasedInjectionWithEarlyGenericsMatchingAndRawFactoryMethod() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawFactoryMethodRepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(0); @@ -689,13 +688,13 @@ public void genericsBasedInjectionWithEarlyGenericsMatchingAndRawFactoryMethod() } @Test - public void genericsBasedInjectionWithLateGenericsMatchingAndRawFactoryMethod() { + void genericsBasedInjectionWithLateGenericsMatchingAndRawFactoryMethod() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawFactoryMethodRepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -707,12 +706,12 @@ public void genericsBasedInjectionWithLateGenericsMatchingAndRawFactoryMethod() } @Test - public void genericsBasedInjectionWithEarlyGenericsMatchingAndRawInstance() { + void genericsBasedInjectionWithEarlyGenericsMatchingAndRawInstance() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawInstanceRepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -724,13 +723,13 @@ public void genericsBasedInjectionWithEarlyGenericsMatchingAndRawInstance() { } @Test - public void genericsBasedInjectionWithLateGenericsMatchingAndRawInstance() { + void genericsBasedInjectionWithLateGenericsMatchingAndRawInstance() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawInstanceRepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -742,7 +741,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingAndRawInstance() { } @Test - public void genericsBasedInjectionWithEarlyGenericsMatchingOnCglibProxy() { + void genericsBasedInjectionWithEarlyGenericsMatchingOnCglibProxy() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); @@ -752,7 +751,7 @@ public void genericsBasedInjectionWithEarlyGenericsMatchingOnCglibProxy() { beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -766,7 +765,7 @@ public void genericsBasedInjectionWithEarlyGenericsMatchingOnCglibProxy() { } @Test - public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxy() { + void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxy() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); @@ -777,7 +776,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxy() { beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -791,7 +790,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxy() { } @Test - public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawFactoryMethod() { + void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawFactoryMethod() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawFactoryMethodRepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); @@ -802,7 +801,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawFact beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -816,7 +815,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawFact } @Test - public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawInstance() { + void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawInstance() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawInstanceRepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); @@ -827,7 +826,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawInst beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(Repository.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(Repository.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -841,7 +840,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnCglibProxyAndRawInst } @Test - public void genericsBasedInjectionWithEarlyGenericsMatchingOnJdkProxy() { + void genericsBasedInjectionWithEarlyGenericsMatchingOnJdkProxy() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); @@ -850,7 +849,7 @@ public void genericsBasedInjectionWithEarlyGenericsMatchingOnJdkProxy() { beanFactory.registerSingleton("traceInterceptor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); String[] beanNames = beanFactory.getBeanNamesForType(RepositoryInterface.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -864,7 +863,7 @@ public void genericsBasedInjectionWithEarlyGenericsMatchingOnJdkProxy() { } @Test - public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxy() { + void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxy() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); @@ -874,7 +873,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxy() { beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(RepositoryInterface.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -888,7 +887,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxy() { } @Test - public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawFactoryMethod() { + void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawFactoryMethod() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawFactoryMethodRepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); @@ -898,7 +897,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawFactor beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(RepositoryInterface.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -912,7 +911,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawFactor } @Test - public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawInstance() { + void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawInstance() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(RawInstanceRepositoryConfiguration.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); @@ -922,7 +921,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawInstan beanFactory.preInstantiateSingletons(); String[] beanNames = beanFactory.getBeanNamesForType(RepositoryInterface.class); - assertThat(ObjectUtils.containsElement(beanNames, "stringRepo")).isTrue(); + assertThat(beanNames).contains("stringRepo"); beanNames = beanFactory.getBeanNamesForType(ResolvableType.forClassWithGenerics(RepositoryInterface.class, String.class)); assertThat(beanNames.length).isEqualTo(1); @@ -936,7 +935,7 @@ public void genericsBasedInjectionWithLateGenericsMatchingOnJdkProxyAndRawInstan } @Test - public void testSelfReferenceExclusionForFactoryMethodOnSameBean() { + void testSelfReferenceExclusionForFactoryMethodOnSameBean() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -950,7 +949,7 @@ public void testSelfReferenceExclusionForFactoryMethodOnSameBean() { } @Test - public void testConfigWithDefaultMethods() { + void testConfigWithDefaultMethods() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -964,7 +963,7 @@ public void testConfigWithDefaultMethods() { } @Test - public void testConfigWithDefaultMethodsUsingAsm() { + void testConfigWithDefaultMethodsUsingAsm() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -978,7 +977,7 @@ public void testConfigWithDefaultMethodsUsingAsm() { } @Test - public void testCircularDependency() { + void testCircularDependency() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); bpp.setBeanFactory(beanFactory); beanFactory.addBeanPostProcessor(bpp); @@ -991,38 +990,38 @@ public void testCircularDependency() { } @Test - public void testCircularDependencyWithApplicationContext() { + void testCircularDependencyWithApplicationContext() { assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> new AnnotationConfigApplicationContext(A.class, AStrich.class)) .withMessageContaining("Circular reference"); } @Test - public void testPrototypeArgumentThroughBeanMethodCall() { + void testPrototypeArgumentThroughBeanMethodCall() { ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithPrototype.class); ctx.getBean(FooFactory.class).createFoo(new BarArgument()); } @Test - public void testSingletonArgumentThroughBeanMethodCall() { + void testSingletonArgumentThroughBeanMethodCall() { ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithSingleton.class); ctx.getBean(FooFactory.class).createFoo(new BarArgument()); } @Test - public void testNullArgumentThroughBeanMethodCall() { + void testNullArgumentThroughBeanMethodCall() { ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanArgumentConfigWithNull.class); ctx.getBean("aFoo"); } @Test - public void testInjectionPointMatchForNarrowTargetReturnType() { + void testInjectionPointMatchForNarrowTargetReturnType() { ApplicationContext ctx = new AnnotationConfigApplicationContext(FooBarConfiguration.class); assertThat(ctx.getBean(FooImpl.class).bar).isSameAs(ctx.getBean(BarImpl.class)); } @Test - public void testVarargOnBeanMethod() { + void testVarargOnBeanMethod() { ApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class, TestBean.class); VarargConfiguration bean = ctx.getBean(VarargConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1031,7 +1030,7 @@ public void testVarargOnBeanMethod() { } @Test - public void testEmptyVarargOnBeanMethod() { + void testEmptyVarargOnBeanMethod() { ApplicationContext ctx = new AnnotationConfigApplicationContext(VarargConfiguration.class); VarargConfiguration bean = ctx.getBean(VarargConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1039,7 +1038,7 @@ public void testEmptyVarargOnBeanMethod() { } @Test - public void testCollectionArgumentOnBeanMethod() { + void testCollectionArgumentOnBeanMethod() { ApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class, TestBean.class); CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1048,7 +1047,7 @@ public void testCollectionArgumentOnBeanMethod() { } @Test - public void testEmptyCollectionArgumentOnBeanMethod() { + void testEmptyCollectionArgumentOnBeanMethod() { ApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class); CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1056,7 +1055,7 @@ public void testEmptyCollectionArgumentOnBeanMethod() { } @Test - public void testMapArgumentOnBeanMethod() { + void testMapArgumentOnBeanMethod() { ApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class, DummyRunnable.class); MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1065,7 +1064,7 @@ public void testMapArgumentOnBeanMethod() { } @Test - public void testEmptyMapArgumentOnBeanMethod() { + void testEmptyMapArgumentOnBeanMethod() { ApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class); MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1073,7 +1072,7 @@ public void testEmptyMapArgumentOnBeanMethod() { } @Test - public void testCollectionInjectionFromSameConfigurationClass() { + void testCollectionInjectionFromSameConfigurationClass() { ApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionInjectionConfiguration.class); CollectionInjectionConfiguration bean = ctx.getBean(CollectionInjectionConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1082,7 +1081,7 @@ public void testCollectionInjectionFromSameConfigurationClass() { } @Test - public void testMapInjectionFromSameConfigurationClass() { + void testMapInjectionFromSameConfigurationClass() { ApplicationContext ctx = new AnnotationConfigApplicationContext(MapInjectionConfiguration.class); MapInjectionConfiguration bean = ctx.getBean(MapInjectionConfiguration.class); assertThat(bean.testBeans).isNotNull(); @@ -1091,7 +1090,7 @@ public void testMapInjectionFromSameConfigurationClass() { } @Test - public void testBeanLookupFromSameConfigurationClass() { + void testBeanLookupFromSameConfigurationClass() { ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanLookupConfiguration.class); BeanLookupConfiguration bean = ctx.getBean(BeanLookupConfiguration.class); assertThat(bean.getTestBean()).isNotNull(); @@ -1099,7 +1098,7 @@ public void testBeanLookupFromSameConfigurationClass() { } @Test - public void testNameClashBetweenConfigurationClassAndBean() { + void testNameClashBetweenConfigurationClassAndBean() { assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> { ApplicationContext ctx = new AnnotationConfigApplicationContext(MyTestBean.class); ctx.getBean("myTestBean", TestBean.class); @@ -1107,7 +1106,7 @@ public void testNameClashBetweenConfigurationClassAndBean() { } @Test - public void testBeanDefinitionRegistryPostProcessorConfig() { + void testBeanDefinitionRegistryPostProcessorConfig() { ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanDefinitionRegistryPostProcessorConfig.class); boolean condition = ctx.getBean("myTestBean") instanceof TestBean; assertThat(condition).isTrue(); From a1320cd450089b88eb689686dbb67bed05bdf8c9 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 26 Nov 2020 14:51:27 +0100 Subject: [PATCH 0091/1294] Add SSE support to WebMvc.fn This commit adds support for sending Server-Sent Events in WebMvc.fn, through the ServerResponse.sse method that takes a SseBuilder DSL. It also includes reference documentation. Closes gh-25920 --- .../server/DelegatingServerHttpResponse.java | 79 +++++ .../function/AbstractServerResponse.java | 141 +++++++++ .../DefaultEntityResponseBuilder.java | 3 +- .../DefaultRenderingResponseBuilder.java | 3 +- .../DefaultServerResponseBuilder.java | 112 ------- .../web/servlet/function/ServerResponse.java | 168 +++++++++- .../servlet/function/SseServerResponse.java | 292 ++++++++++++++++++ ...ResponseBodyEmitterReturnValueHandler.java | 28 +- .../function/SseServerResponseTests.java | 145 +++++++++ src/docs/asciidoc/web/webmvc-functional.adoc | 54 ++++ 10 files changed, 874 insertions(+), 151 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/server/DelegatingServerHttpResponse.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java diff --git a/spring-web/src/main/java/org/springframework/http/server/DelegatingServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/DelegatingServerHttpResponse.java new file mode 100644 index 000000000000..43dc6e57b531 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/DelegatingServerHttpResponse.java @@ -0,0 +1,79 @@ +/* + * 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.http.server; + +import java.io.IOException; +import java.io.OutputStream; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +/** + * Implementation of {@code ServerHttpResponse} that delegates all calls to a + * given target {@code ServerHttpResponse}. + * + * @author Arjen Poutsma + * @since 5.3.2 + */ +public class DelegatingServerHttpResponse implements ServerHttpResponse { + + private final ServerHttpResponse delegate; + + /** + * Create a new {@code DelegatingServerHttpResponse}. + * @param delegate the response to delegate to + */ + public DelegatingServerHttpResponse(ServerHttpResponse delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + /** + * Returns the target response that this response delegates to. + * @return the delegate + */ + public ServerHttpResponse getDelegate() { + return this.delegate; + } + + @Override + public void setStatusCode(HttpStatus status) { + this.delegate.setStatusCode(status); + } + + @Override + public void flush() throws IOException { + this.delegate.flush(); + } + + @Override + public void close() { + this.delegate.close(); + } + + @Override + public OutputStream getBody() throws IOException { + return this.delegate.getBody(); + } + + @Override + public HttpHeaders getHeaders() { + return this.delegate.getHeaders(); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java new file mode 100644 index 000000000000..d191c05c298a --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AbstractServerResponse.java @@ -0,0 +1,141 @@ +/* + * 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.function; + +import java.io.IOException; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.ModelAndView; + +/** + * Abstract base class for {@link ServerResponse} implementations. + * + * @author Arjen Poutsma + * @since 5.3.2 + */ +abstract class AbstractServerResponse extends ErrorHandlingServerResponse { + + private static final Set SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD); + + final int statusCode; + + private final HttpHeaders headers; + + private final MultiValueMap cookies; + + protected AbstractServerResponse( + int statusCode, HttpHeaders headers, MultiValueMap cookies) { + + this.statusCode = statusCode; + this.headers = HttpHeaders.readOnlyHttpHeaders(headers); + this.cookies = + CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies)); + } + + @Override + public final HttpStatus statusCode() { + return HttpStatus.valueOf(this.statusCode); + } + + @Override + public int rawStatusCode() { + return this.statusCode; + } + + @Override + public final HttpHeaders headers() { + return this.headers; + } + + @Override + public MultiValueMap cookies() { + return this.cookies; + } + + @Override + public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, + Context context) throws ServletException, IOException { + + try { + writeStatusAndHeaders(response); + + long lastModified = headers().getLastModified(); + ServletWebRequest servletWebRequest = new ServletWebRequest(request, response); + HttpMethod httpMethod = HttpMethod.resolve(request.getMethod()); + if (SAFE_METHODS.contains(httpMethod) && + servletWebRequest.checkNotModified(headers().getETag(), lastModified)) { + return null; + } + else { + return writeToInternal(request, response, context); + } + } + catch (Throwable throwable) { + return handleError(throwable, request, response, context); + } + } + + private void writeStatusAndHeaders(HttpServletResponse response) { + response.setStatus(this.statusCode); + writeHeaders(response); + writeCookies(response); + } + + private void writeHeaders(HttpServletResponse servletResponse) { + this.headers.forEach((headerName, headerValues) -> { + for (String headerValue : headerValues) { + servletResponse.addHeader(headerName, headerValue); + } + }); + // HttpServletResponse exposes some headers as properties: we should include those if not already present + if (servletResponse.getContentType() == null && this.headers.getContentType() != null) { + servletResponse.setContentType(this.headers.getContentType().toString()); + } + if (servletResponse.getCharacterEncoding() == null && + this.headers.getContentType() != null && + this.headers.getContentType().getCharset() != null) { + servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name()); + } + } + + private void writeCookies(HttpServletResponse servletResponse) { + this.cookies.values().stream() + .flatMap(Collection::stream) + .forEach(servletResponse::addCookie); + } + + @Nullable + protected abstract ModelAndView writeToInternal( + HttpServletRequest request, HttpServletResponse response, Context context) + throws ServletException, IOException; + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index e27b0f48c1fa..f28b1d81745d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -237,8 +237,7 @@ public static EntityResponse.Builder fromObject(T t, ParameterizedTypeRef /** * Default {@link EntityResponse} implementation for synchronous bodies. */ - private static class DefaultEntityResponse extends DefaultServerResponseBuilder.AbstractServerResponse - implements EntityResponse { + private static class DefaultEntityResponse extends AbstractServerResponse implements EntityResponse { private final T entity; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultRenderingResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultRenderingResponseBuilder.java index 236c0d506524..8b6a4faa140f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultRenderingResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultRenderingResponseBuilder.java @@ -150,8 +150,7 @@ public RenderingResponse build() { } - private static final class DefaultRenderingResponse extends DefaultServerResponseBuilder.AbstractServerResponse - implements RenderingResponse { + private static final class DefaultRenderingResponse extends AbstractServerResponse implements RenderingResponse { private final String name; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerResponseBuilder.java index dcba35f9e33f..8f9dbcde88cd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerResponseBuilder.java @@ -16,20 +16,16 @@ package org.springframework.web.servlet.function; -import java.io.IOException; import java.net.URI; import java.time.Instant; import java.time.ZonedDateTime; import java.util.Arrays; -import java.util.Collection; -import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Consumer; -import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -40,12 +36,9 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.servlet.ModelAndView; /** @@ -224,111 +217,6 @@ public ServerResponse render(String name, Map model) { } - /** - * Abstract base class for {@link ServerResponse} implementations. - */ - abstract static class AbstractServerResponse extends ErrorHandlingServerResponse { - - private static final Set SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD); - - - final int statusCode; - - private final HttpHeaders headers; - - private final MultiValueMap cookies; - - - protected AbstractServerResponse( - int statusCode, HttpHeaders headers, MultiValueMap cookies) { - - this.statusCode = statusCode; - this.headers = HttpHeaders.readOnlyHttpHeaders(headers); - this.cookies = - CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies)); - } - - - @Override - public final HttpStatus statusCode() { - return HttpStatus.valueOf(this.statusCode); - } - - @Override - public int rawStatusCode() { - return this.statusCode; - } - - @Override - public final HttpHeaders headers() { - return this.headers; - } - - @Override - public MultiValueMap cookies() { - return this.cookies; - } - - @Override - public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, - Context context) throws ServletException, IOException { - - try { - writeStatusAndHeaders(response); - - long lastModified = headers().getLastModified(); - ServletWebRequest servletWebRequest = new ServletWebRequest(request, response); - HttpMethod httpMethod = HttpMethod.resolve(request.getMethod()); - if (SAFE_METHODS.contains(httpMethod) && - servletWebRequest.checkNotModified(headers().getETag(), lastModified)) { - return null; - } - else { - return writeToInternal(request, response, context); - } - } - catch (Throwable throwable) { - return handleError(throwable, request, response, context); - } - } - - private void writeStatusAndHeaders(HttpServletResponse response) { - response.setStatus(this.statusCode); - writeHeaders(response); - writeCookies(response); - } - - private void writeHeaders(HttpServletResponse servletResponse) { - this.headers.forEach((headerName, headerValues) -> { - for (String headerValue : headerValues) { - servletResponse.addHeader(headerName, headerValue); - } - }); - // HttpServletResponse exposes some headers as properties: we should include those if not already present - if (servletResponse.getContentType() == null && this.headers.getContentType() != null) { - servletResponse.setContentType(this.headers.getContentType().toString()); - } - if (servletResponse.getCharacterEncoding() == null && - this.headers.getContentType() != null && - this.headers.getContentType().getCharset() != null) { - servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name()); - } - } - - private void writeCookies(HttpServletResponse servletResponse) { - this.cookies.values().stream() - .flatMap(Collection::stream) - .forEach(servletResponse::addCookie); - } - - @Nullable - protected abstract ModelAndView writeToInternal( - HttpServletRequest request, HttpServletResponse response, Context context) - throws ServletException, IOException; - - } - - private static class WriterFunctionResponse extends AbstractServerResponse { private final BiFunction writeFunction; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java index ad932104989b..85823b75b9ca 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java @@ -230,11 +230,6 @@ static BodyBuilder unprocessableEntity() { *

    This method can be used to set the response status code, headers, and * body based on an asynchronous result. If only the body is asynchronous, * {@link BodyBuilder#body(Object)} can be used instead. - * - *

    Note that - * {@linkplain RenderingResponse rendering responses}, as returned by - * {@link BodyBuilder#render}, are not supported as value - * for {@code asyncResponse}. Use WebFlux.fn for asynchronous rendering. * @param asyncResponse a {@code CompletableFuture} or * {@code Publisher} * @return the asynchronous response @@ -255,11 +250,6 @@ static ServerResponse async(Object asyncResponse) { *

    This method can be used to set the response status code, headers, and * body based on an asynchronous result. If only the body is asynchronous, * {@link BodyBuilder#body(Object)} can be used instead. - * - *

    Note that - * {@linkplain RenderingResponse rendering responses}, as returned by - * {@link BodyBuilder#render}, are not supported as value - * for {@code asyncResponse}. Use WebFlux.fn for asynchronous rendering. * @param asyncResponse a {@code CompletableFuture} or * {@code Publisher} * @param timeout maximum time period to wait for before timing out @@ -270,6 +260,65 @@ static ServerResponse async(Object asyncResponse, Duration timeout) { return AsyncServerResponse.create(asyncResponse, timeout); } + /** + * Create a server-sent event response. The {@link SseBuilder} provided to + * {@code consumer} can be used to build and send events. + * + *

    For example: + *

    +	 * public ServerResponse handleSse(ServerRequest request) {
    +	 *     return ServerResponse.sse(sse -> sse.send("Hello World!"));
    +	 * }
    +	 * 
    + * + *

    or, to set both the id and event type: + *

    +	 * public ServerResponse handleSse(ServerRequest request) {
    +	 *     return ServerResponse.sse(sse -> sse
    +	 *         .id("42)
    +	 *         .event("event")
    +	 *         .send("Hello World!"));
    +	 * }
    +	 * 
    + * @param consumer consumer that will be provided with an event builder + * @return the server-side event response + * @since 5.3.2 + * @see Server-Sent Events + */ + static ServerResponse sse(Consumer consumer) { + return SseServerResponse.create(consumer, null); + } + + /** + * Create a server-sent event response. The {@link SseBuilder} provided to + * {@code consumer} can be used to build and send events. + * + *

    For example: + *

    +	 * public ServerResponse handleSse(ServerRequest request) {
    +	 *     return ServerResponse.sse(sse -> sse.send("Hello World!"));
    +	 * }
    +	 * 
    + * + *

    or, to set both the id and event type: + *

    +	 * public ServerResponse handleSse(ServerRequest request) {
    +	 *     return ServerResponse.sse(sse -> sse
    +	 *         .id("42)
    +	 *         .event("event")
    +	 *         .send("Hello World!"));
    +	 * }
    +	 * 
    + * @param consumer consumer that will be provided with an event builder + * @param timeout maximum time period to wait before timing out + * @return the server-side event response + * @since 5.3.2 + * @see Server-Sent Events + */ + static ServerResponse sse(Consumer consumer, Duration timeout) { + return SseServerResponse.create(consumer, timeout); + } + /** * Defines a builder that adds headers to the response. @@ -473,6 +522,105 @@ interface BodyBuilder extends HeadersBuilder { } + /** + * Defines a builder for a body that sends server-sent events. + * + * @since 5.3.2 + */ + interface SseBuilder { + + /** + * Sends the given object as a server-sent event. + * Strings will be sent as UTF-8 encoded bytes, and other objects will + * be converted into JSON using + * {@linkplain HttpMessageConverter message converters}. + * + *

    This convenience method has the same effect as + * {@link #data(Object)}. + * @param object the object to send + * @throws IOException in case of I/O errors + */ + void send(Object object) throws IOException; + + /** + * Add an SSE "id" line. + * @param id the event identifier + * @return this builder + */ + SseBuilder id(String id); + + /** + * Add an SSE "event" line. + * @param eventName the event name + * @return this builder + */ + SseBuilder event(String eventName); + + /** + * Add an SSE "retry" line. + * @param duration the duration to convert into millis + * @return this builder + */ + SseBuilder retry(Duration duration); + + /** + * Add an SSE comment. + * @param comment the comment + * @return this builder + */ + SseBuilder comment(String comment); + + /** + * Add an SSE "data" line for the given object and sends the built + * server-sent event to the client. + * Strings will be sent as UTF-8 encoded bytes, and other objects will + * be converted into JSON using + * {@linkplain HttpMessageConverter message converters}. + * @param object the object to send as data + * @throws IOException in case of I/O errors + */ + void data(Object object) throws IOException; + + /** + * Completes the event stream with the given error. + * + *

    The throwable is dispatched back into Spring MVC, and passed to + * its exception handling mechanism. Since the response has + * been committed by this point, the response status can not change. + * @param t the throwable to dispatch + */ + void error(Throwable t); + + /** + * Completes the event stream. + */ + void complete(); + + /** + * Register a callback to be invoked when an SSE request times + * out. + * @param onTimeout the callback to invoke on timeout + * @return this builder + */ + SseBuilder onTimeout(Runnable onTimeout); + + /** + * Register a callback to be invoked when an error occurs during SSE + * processing. + * @param onError the callback to invoke on error + * @return this builder + */ + SseBuilder onError(Consumer onError); + + /** + * Register a callback to be invoked when the SSE request completes. + * @param onCompletion the callback to invoked on completion + * @return this builder + */ + SseBuilder onComplete(Runnable onCompletion); + } + + /** * Defines the context used during the {@link #writeTo(HttpServletRequest, HttpServletResponse, Context)}. */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java new file mode 100644 index 000000000000..a641033b6ce1 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java @@ -0,0 +1,292 @@ +/* + * 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.function; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.DelegatingServerHttpResponse; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.servlet.ModelAndView; + +/** + * Implementation of {@link ServerResponse} for sending + * Server-Sent Events. + * + * @author Arjen Poutsma + * @since 5.3.2 + */ +final class SseServerResponse extends AbstractServerResponse { + + private final Consumer sseConsumer; + + @Nullable + private final Duration timeout; + + + private SseServerResponse(Consumer sseConsumer, @Nullable Duration timeout) { + super(200, createHeaders(), emptyCookies()); + this.sseConsumer = sseConsumer; + this.timeout = timeout; + } + + private static HttpHeaders createHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_EVENT_STREAM); + headers.setCacheControl(CacheControl.noCache()); + return headers; + } + + private static MultiValueMap emptyCookies() { + return CollectionUtils.toMultiValueMap(Collections.emptyMap()); + } + + + @Nullable + @Override + protected ModelAndView writeToInternal(HttpServletRequest request, HttpServletResponse response, + Context context) throws ServletException, IOException { + + DeferredResult result; + if (this.timeout != null) { + result = new DeferredResult<>(this.timeout.toMillis()); + } + else { + result = new DeferredResult<>(); + } + + AsyncServerResponse.writeAsync(request, response, result); + this.sseConsumer.accept(new DefaultSseBuilder(response, context, result)); + return null; + } + + + public static ServerResponse create(Consumer sseConsumer, @Nullable Duration timeout) { + Assert.notNull(sseConsumer, "SseConsumer must not be null"); + + return new SseServerResponse(sseConsumer, timeout); + } + + + private static final class DefaultSseBuilder implements SseBuilder { + + private static final byte[] NL_NL = new byte[]{'\n', '\n'}; + + + private final ServerHttpResponse outputMessage; + + private final DeferredResult deferredResult; + + private final List> messageConverters; + + private final StringBuilder builder = new StringBuilder(); + + private boolean sendFailed; + + + public DefaultSseBuilder(HttpServletResponse response, Context context, DeferredResult deferredResult) { + this.outputMessage = new ServletServerHttpResponse(response); + this.deferredResult = deferredResult; + this.messageConverters = context.messageConverters(); + } + + @Override + public void send(Object object) throws IOException { + data(object); + } + + @Override + public SseBuilder id(String id) { + Assert.hasLength(id, "Id must not be empty"); + return field("id", id); + } + + @Override + public SseBuilder event(String eventName) { + Assert.hasLength(eventName, "Name must not be empty"); + return field("event", eventName); + } + + @Override + public SseBuilder retry(Duration duration) { + Assert.notNull(duration, "Duration must not be null"); + String millis = Long.toString(duration.toMillis()); + return field("retry", millis); + } + + @Override + public SseBuilder comment(String comment) { + Assert.hasLength(comment, "Comment must not be empty"); + String[] lines = comment.split("\n"); + for (String line : lines) { + field("", line); + } + return this; + } + + private SseBuilder field(String name, String value) { + this.builder.append(name).append(':').append(value).append('\n'); + return this; + } + + @Override + public void data(Object object) throws IOException { + Assert.notNull(object, "Object must not be null"); + + if (object instanceof String) { + writeString((String) object); + } + else { + writeObject(object); + } + } + + private void writeString(String string) throws IOException { + String[] lines = string.split("\n"); + for (String line : lines) { + field("data", line); + } + this.builder.append('\n'); + + try { + OutputStream body = this.outputMessage.getBody(); + body.write(builderBytes()); + body.flush(); + } + catch (IOException ex) { + this.sendFailed = true; + throw ex; + } + finally { + this.builder.setLength(0); + } + } + + @SuppressWarnings("unchecked") + private void writeObject(Object data) throws IOException { + this.builder.append("data:"); + try { + this.outputMessage.getBody().write(builderBytes()); + + Class dataClass = data.getClass(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter.canWrite(dataClass, MediaType.APPLICATION_JSON)) { + HttpMessageConverter objectConverter = (HttpMessageConverter) converter; + ServerHttpResponse response = new MutableHeadersServerHttpResponse(this.outputMessage); + objectConverter.write(data, MediaType.APPLICATION_JSON, response); + this.outputMessage.getBody().write(NL_NL); + this.outputMessage.flush(); + return; + } + } + } + catch (IOException ex) { + this.sendFailed = true; + throw ex; + } + finally { + this.builder.setLength(0); + } + } + + private byte[] builderBytes() { + return this.builder.toString().getBytes(StandardCharsets.UTF_8); + } + + @Override + public void error(Throwable t) { + if (this.sendFailed) { + return; + } + this.deferredResult.setErrorResult(t); + } + + @Override + public void complete() { + if (this.sendFailed) { + return; + } + try { + this.outputMessage.flush(); + this.deferredResult.setResult(null); + } + catch (IOException ex) { + this.deferredResult.setErrorResult(ex); + } + } + + @Override + public SseBuilder onTimeout(Runnable onTimeout) { + this.deferredResult.onTimeout(onTimeout); + return this; + } + + @Override + public SseBuilder onError(Consumer onError) { + this.deferredResult.onError(onError); + return this; + } + + @Override + public SseBuilder onComplete(Runnable onCompletion) { + this.deferredResult.onCompletion(onCompletion); + return this; + } + + + /** + * Wrap to silently ignore header changes HttpMessageConverter's that would + * otherwise cause HttpHeaders to raise exceptions. + */ + private static final class MutableHeadersServerHttpResponse extends DelegatingServerHttpResponse { + + private final HttpHeaders mutableHeaders = new HttpHeaders(); + + public MutableHeadersServerHttpResponse(ServerHttpResponse delegate) { + super(delegate); + this.mutableHeaders.putAll(delegate.getHeaders()); + } + + @Override + public HttpHeaders getHeaders() { + return this.mutableHeaders; + } + + } + + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java index 65a92f4946f5..6688fe5b5afa 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java @@ -17,7 +17,6 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; -import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -31,11 +30,11 @@ import org.springframework.core.ResolvableType; import org.springframework.core.task.TaskExecutor; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.server.DelegatingServerHttpResponse; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.lang.Nullable; @@ -255,41 +254,20 @@ public void onCompletion(Runnable callback) { * Wrap to silently ignore header changes HttpMessageConverter's that would * otherwise cause HttpHeaders to raise exceptions. */ - private static class StreamingServletServerHttpResponse implements ServerHttpResponse { - - private final ServerHttpResponse delegate; + private static class StreamingServletServerHttpResponse extends DelegatingServerHttpResponse { private final HttpHeaders mutableHeaders = new HttpHeaders(); public StreamingServletServerHttpResponse(ServerHttpResponse delegate) { - this.delegate = delegate; + super(delegate); this.mutableHeaders.putAll(delegate.getHeaders()); } - @Override - public void setStatusCode(HttpStatus status) { - this.delegate.setStatusCode(status); - } - @Override public HttpHeaders getHeaders() { return this.mutableHeaders; } - @Override - public OutputStream getBody() throws IOException { - return this.delegate.getBody(); - } - - @Override - public void flush() throws IOException { - this.delegate.flush(); - } - - @Override - public void close() { - this.delegate.close(); - } } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java new file mode 100644 index 000000000000..ca035489fb1a --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java @@ -0,0 +1,145 @@ +/* + * 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.function; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.time.Duration; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.testfixture.servlet.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class SseServerResponseTests { + + private MockHttpServletRequest mockRequest; + + private MockHttpServletResponse mockResponse; + + @BeforeEach + void setUp() { + this.mockRequest = new MockHttpServletRequest("GET", "https://example.com/"); + this.mockRequest.setAsyncSupported(true); + this.mockResponse = new MockHttpServletResponse(); + } + + @Test + void sendString() throws Exception { + String body = "foo bar"; + ServerResponse response = ServerResponse.sse(sse -> { + try { + sse.send(body); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + + ServerResponse.Context context = Collections::emptyList; + + ModelAndView mav = response.writeTo(this.mockRequest, this.mockResponse, context); + assertThat(mav).isNull(); + + String expected = "data:" + body + "\n\n"; + assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected); + } + + @Test + void sendObject() throws Exception { + Person person = new Person("John Doe", 42); + ServerResponse response = ServerResponse.sse(sse -> { + try { + sse.send(person); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + + ServerResponse.Context context = () -> Collections.singletonList(new MappingJackson2HttpMessageConverter()); + + ModelAndView mav = response.writeTo(this.mockRequest, this.mockResponse, context); + assertThat(mav).isNull(); + + String expected = "data:{\"name\":\"John Doe\",\"age\":42}\n\n"; + assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected); + } + + @Test + public void builder() throws Exception { + String body = "foo bar"; + ServerResponse response = ServerResponse.sse(sse -> { + try { + sse.id("id") + .event("name") + .comment("comment line 1\ncomment line 2") + .retry(Duration.ofSeconds(1)) + .data("data"); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + + ServerResponse.Context context = Collections::emptyList; + + ModelAndView mav = response.writeTo(this.mockRequest, this.mockResponse, context); + assertThat(mav).isNull(); + + String expected = "id:id\n" + + "event:name\n" + + ":comment line 1\n" + + ":comment line 2\n" + + "retry:1000\n" + + "data:data\n\n"; + assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected); + } + + + private static final class Person { + + private final String name; + + private final int age; + + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return this.name; + } + + public int getAge() { + return this.age; + } + } + + +} diff --git a/src/docs/asciidoc/web/webmvc-functional.adoc b/src/docs/asciidoc/web/webmvc-functional.adoc index ded811f373bb..56de64c38c65 100644 --- a/src/docs/asciidoc/web/webmvc-functional.adoc +++ b/src/docs/asciidoc/web/webmvc-functional.adoc @@ -229,6 +229,60 @@ Mono asyncResponse = webClient.get().retrieve().bodyToMono(Perso ServerResponse.async(asyncResponse); ---- +https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the +static `sse` method on `ServerResponse`. The builder provided by that method +allows you to send Strings, or other objects as JSON. For example: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java +---- + public RouterFunction sse() { + return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> { + // Save the sseBuilder object somewhere.. + })); + } + + // In some other thread, sending a String + sseBuilder.send("Hello world"); + + // Or an object, which will be transformed into JSON + Person person = ... + sseBuilder.send(person); + + // Customize the event by using the other methods + sseBuilder.id("42") + .event("sse event") + .data(person); + + // and done at some point + sseBuilder.complete(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + fun sse(): RouterFunction = router { + GET("/sse") { request -> ServerResponse.sse { sseBuilder -> + // Save the sseBuilder object somewhere.. + } + } + + // In some other thread, sending a String + sseBuilder.send("Hello world") + + // Or an object, which will be transformed into JSON + val person = ... + sseBuilder.send(person) + + // Customize the event by using the other methods + sseBuilder.id("42") + .event("sse event") + .data(person) + + // and done at some point + sseBuilder.complete() +---- + + [[webmvc-fn-handler-classes]] === Handler Classes From 227d85a6b41677d5b7b8af0e6e10eb982e3d5dda Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 2 Dec 2020 17:22:22 +0100 Subject: [PATCH 0092/1294] Upgrade to Jackson 2.12, Hibernate ORM 5.4.25, Checkstyle 8.38 See gh-25907 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 79e9d8303692..3a9bfdfdb167 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ configure(allprojects) { project -> dependencyManagement { imports { - mavenBom "com.fasterxml.jackson:jackson-bom:2.11.3" + mavenBom "com.fasterxml.jackson:jackson-bom:2.12.0" mavenBom "io.netty:netty-bom:4.1.54.Final" mavenBom "io.projectreactor:reactor-bom:2020.0.2-SNAPSHOT" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" @@ -123,7 +123,7 @@ configure(allprojects) { project -> dependency "net.sf.ehcache:ehcache:2.10.6" dependency "org.ehcache:jcache:1.0.1" dependency "org.ehcache:ehcache:3.4.0" - dependency "org.hibernate:hibernate-core:5.4.24.Final" + dependency "org.hibernate:hibernate-core:5.4.25.Final" dependency "org.hibernate:hibernate-validator:6.1.6.Final" dependency "org.webjars:webjars-locator-core:0.46" dependency "org.webjars:underscorejs:1.8.3" @@ -340,7 +340,7 @@ configure([rootProject] + javaProjects) { project -> } checkstyle { - toolVersion = "8.37" + toolVersion = "8.38" configDirectory.set(rootProject.file("src/checkstyle")) } From 3714d0e401102c4b9215dab3889dd31ba38d979f Mon Sep 17 00:00:00 2001 From: Alex Feigin Date: Mon, 30 Nov 2020 16:40:28 +0200 Subject: [PATCH 0093/1294] Expose future response in new AsyncServerResponse This commit introduces AsyncServerResponse, an extension of ServerResponse that is returned from ServerResponse.async and that allows users to get the future response by calling the block method. This is particularly useful for testing purposes. --- .../servlet/function/AsyncServerResponse.java | 182 ++++------------- .../function/DefaultAsyncServerResponse.java | 191 ++++++++++++++++++ .../DefaultEntityResponseBuilder.java | 6 +- .../web/servlet/function/ServerResponse.java | 4 +- .../servlet/function/SseServerResponse.java | 2 +- .../DefaultAsyncServerResponseTests.java | 39 ++++ 6 files changed, 277 insertions(+), 147 deletions(-) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java index 279a9b165c33..b2fca283a00e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/AsyncServerResponse.java @@ -16,161 +16,61 @@ package org.springframework.web.servlet.function; -import java.io.IOException; import java.time.Duration; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.function.Function; - -import javax.servlet.ServletException; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import org.reactivestreams.Publisher; -import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.MultiValueMap; -import org.springframework.web.context.request.async.AsyncWebRequest; -import org.springframework.web.context.request.async.DeferredResult; -import org.springframework.web.context.request.async.WebAsyncManager; -import org.springframework.web.context.request.async.WebAsyncUtils; -import org.springframework.web.servlet.ModelAndView; /** - * Implementation of {@link ServerResponse} based on a {@link CompletableFuture}. + * Asynchronous subtype of {@link ServerResponse} that exposes the future + * response. * * @author Arjen Poutsma - * @since 5.3 + * @since 5.3.2 * @see ServerResponse#async(Object) */ -final class AsyncServerResponse extends ErrorHandlingServerResponse { - - static final boolean reactiveStreamsPresent = ClassUtils.isPresent( - "org.reactivestreams.Publisher", AsyncServerResponse.class.getClassLoader()); - - - private final CompletableFuture futureResponse; - - @Nullable - private final Duration timeout; - - - private AsyncServerResponse(CompletableFuture futureResponse, @Nullable Duration timeout) { - this.futureResponse = futureResponse; - this.timeout = timeout; - } - - @Override - public HttpStatus statusCode() { - return delegate(ServerResponse::statusCode); - } - - @Override - public int rawStatusCode() { - return delegate(ServerResponse::rawStatusCode); - } - - @Override - public HttpHeaders headers() { - return delegate(ServerResponse::headers); - } - - @Override - public MultiValueMap cookies() { - return delegate(ServerResponse::cookies); +public interface AsyncServerResponse extends ServerResponse { + + /** + * Blocks indefinitely until the future response is obtained. + */ + ServerResponse block(); + + + // Static creation methods + + /** + * Create a {@code AsyncServerResponse} with the given asynchronous response. + * Parameter {@code asyncResponse} can be a + * {@link CompletableFuture CompletableFuture<ServerResponse>} or + * {@link Publisher Publisher<ServerResponse>} (or any + * asynchronous producer of a single {@code ServerResponse} that can be + * adapted via the {@link ReactiveAdapterRegistry}). + * @param asyncResponse a {@code CompletableFuture} or + * {@code Publisher} + * @return the asynchronous response + */ + static AsyncServerResponse create(Object asyncResponse) { + return DefaultAsyncServerResponse.create(asyncResponse, null); } - private R delegate(Function function) { - ServerResponse response = this.futureResponse.getNow(null); - if (response != null) { - return function.apply(response); - } - else { - throw new IllegalStateException("Future ServerResponse has not yet completed"); - } + /** + * Create a (built) response with the given asynchronous response. + * Parameter {@code asyncResponse} can be a + * {@link CompletableFuture CompletableFuture<ServerResponse>} or + * {@link Publisher Publisher<ServerResponse>} (or any + * asynchronous producer of a single {@code ServerResponse} that can be + * adapted via the {@link ReactiveAdapterRegistry}). + * @param asyncResponse a {@code CompletableFuture} or + * {@code Publisher} + * @param timeout maximum time period to wait for before timing out + * @return the asynchronous response + */ + static AsyncServerResponse create(Object asyncResponse, Duration timeout) { + return DefaultAsyncServerResponse.create(asyncResponse, timeout); } - @Nullable - @Override - public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) - throws ServletException, IOException { - - writeAsync(request, response, createDeferredResult()); - return null; - } - - static void writeAsync(HttpServletRequest request, HttpServletResponse response, DeferredResult deferredResult) - throws ServletException, IOException { - - WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); - AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); - asyncManager.setAsyncWebRequest(asyncWebRequest); - try { - asyncManager.startDeferredResultProcessing(deferredResult); - } - catch (IOException | ServletException ex) { - throw ex; - } - catch (Exception ex) { - throw new ServletException("Async processing failed", ex); - } - - } - - private DeferredResult createDeferredResult() { - DeferredResult result; - if (this.timeout != null) { - result = new DeferredResult<>(this.timeout.toMillis()); - } - else { - result = new DeferredResult<>(); - } - this.futureResponse.handle((value, ex) -> { - if (ex != null) { - if (ex instanceof CompletionException && ex.getCause() != null) { - ex = ex.getCause(); - } - result.setErrorResult(ex); - } - else { - result.setResult(value); - } - return null; - }); - return result; - } - - - @SuppressWarnings({"unchecked"}) - public static ServerResponse create(Object o, @Nullable Duration timeout) { - Assert.notNull(o, "Argument to async must not be null"); - - if (o instanceof CompletableFuture) { - CompletableFuture futureResponse = (CompletableFuture) o; - return new AsyncServerResponse(futureResponse, timeout); - } - else if (reactiveStreamsPresent) { - ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); - ReactiveAdapter publisherAdapter = registry.getAdapter(o.getClass()); - if (publisherAdapter != null) { - Publisher publisher = publisherAdapter.toPublisher(o); - ReactiveAdapter futureAdapter = registry.getAdapter(CompletableFuture.class); - if (futureAdapter != null) { - CompletableFuture futureResponse = - (CompletableFuture) futureAdapter.fromPublisher(publisher); - return new AsyncServerResponse(futureResponse, timeout); - } - } - } - throw new IllegalArgumentException("Asynchronous type not supported: " + o.getClass()); - } - - } + diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java new file mode 100644 index 000000000000..0fd283445436 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -0,0 +1,191 @@ +/* + * 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.function; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.reactivestreams.Publisher; + +import org.springframework.core.ReactiveAdapter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.request.async.AsyncWebRequest; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.servlet.ModelAndView; + +/** + * Default {@link AsyncServerResponse} implementation. + * + * @author Arjen Poutsma + * @since 5.3.2 + */ +final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse implements AsyncServerResponse { + + static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + "org.reactivestreams.Publisher", DefaultAsyncServerResponse.class.getClassLoader()); + + private final CompletableFuture futureResponse; + + @Nullable + private final Duration timeout; + + + private DefaultAsyncServerResponse(CompletableFuture futureResponse, @Nullable Duration timeout) { + this.futureResponse = futureResponse; + this.timeout = timeout; + } + + @Override + public ServerResponse block() { + try { + if (this.timeout != null) { + return this.futureResponse.get(this.timeout.toMillis(), TimeUnit.MILLISECONDS); + } + else { + return this.futureResponse.get(); + } + } + catch (InterruptedException | ExecutionException | TimeoutException ex) { + throw new IllegalStateException("Failed to get future response", ex); + } + } + + @Override + public HttpStatus statusCode() { + return delegate(ServerResponse::statusCode); + } + + @Override + public int rawStatusCode() { + return delegate(ServerResponse::rawStatusCode); + } + + @Override + public HttpHeaders headers() { + return delegate(ServerResponse::headers); + } + + @Override + public MultiValueMap cookies() { + return delegate(ServerResponse::cookies); + } + + private R delegate(Function function) { + ServerResponse response = this.futureResponse.getNow(null); + if (response != null) { + return function.apply(response); + } + else { + throw new IllegalStateException("Future ServerResponse has not yet completed"); + } + } + + @Nullable + @Override + public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context) + throws ServletException, IOException { + + writeAsync(request, response, createDeferredResult()); + return null; + } + + static void writeAsync(HttpServletRequest request, HttpServletResponse response, DeferredResult deferredResult) + throws ServletException, IOException { + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); + asyncManager.setAsyncWebRequest(asyncWebRequest); + try { + asyncManager.startDeferredResultProcessing(deferredResult); + } + catch (IOException | ServletException ex) { + throw ex; + } + catch (Exception ex) { + throw new ServletException("Async processing failed", ex); + } + + } + + private DeferredResult createDeferredResult() { + DeferredResult result; + if (this.timeout != null) { + result = new DeferredResult<>(this.timeout.toMillis()); + } + else { + result = new DeferredResult<>(); + } + this.futureResponse.handle((value, ex) -> { + if (ex != null) { + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); + } + result.setErrorResult(ex); + } + else { + result.setResult(value); + } + return null; + }); + return result; + } + + @SuppressWarnings({"unchecked"}) + public static AsyncServerResponse create(Object o, @Nullable Duration timeout) { + Assert.notNull(o, "Argument to async must not be null"); + + if (o instanceof CompletableFuture) { + CompletableFuture futureResponse = (CompletableFuture) o; + return new DefaultAsyncServerResponse(futureResponse, timeout); + } + else if (reactiveStreamsPresent) { + ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); + ReactiveAdapter publisherAdapter = registry.getAdapter(o.getClass()); + if (publisherAdapter != null) { + Publisher publisher = publisherAdapter.toPublisher(o); + ReactiveAdapter futureAdapter = registry.getAdapter(CompletableFuture.class); + if (futureAdapter != null) { + CompletableFuture futureResponse = + (CompletableFuture) futureAdapter.fromPublisher(publisher); + return new DefaultAsyncServerResponse(futureResponse, timeout); + } + } + } + throw new IllegalArgumentException("Asynchronous type not supported: " + o.getClass()); + } + + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java index f28b1d81745d..4dfad874a4ef 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java @@ -208,7 +208,7 @@ public EntityResponse build() { return new CompletionStageEntityResponse(this.status, this.headers, this.cookies, completionStage, this.entityType); } - else if (AsyncServerResponse.reactiveStreamsPresent) { + else if (DefaultAsyncServerResponse.reactiveStreamsPresent) { ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(this.entity.getClass()); if (adapter != null) { Publisher publisher = adapter.toPublisher(this.entity); @@ -362,7 +362,7 @@ protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpSe Context context) throws ServletException, IOException { DeferredResult deferredResult = createDeferredResult(servletRequest, servletResponse, context); - AsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); + DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); return null; } @@ -410,7 +410,7 @@ protected ModelAndView writeToInternal(HttpServletRequest servletRequest, HttpSe Context context) throws ServletException, IOException { DeferredResult deferredResult = new DeferredResult<>(); - AsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); + DefaultAsyncServerResponse.writeAsync(servletRequest, servletResponse, deferredResult); entity().subscribe(new DeferredResultSubscriber(servletRequest, servletResponse, context, deferredResult)); return null; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java index 85823b75b9ca..11efe5b02232 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java @@ -236,7 +236,7 @@ static BodyBuilder unprocessableEntity() { * @since 5.3 */ static ServerResponse async(Object asyncResponse) { - return AsyncServerResponse.create(asyncResponse, null); + return DefaultAsyncServerResponse.create(asyncResponse, null); } /** @@ -257,7 +257,7 @@ static ServerResponse async(Object asyncResponse) { * @since 5.3.2 */ static ServerResponse async(Object asyncResponse, Duration timeout) { - return AsyncServerResponse.create(asyncResponse, timeout); + return DefaultAsyncServerResponse.create(asyncResponse, timeout); } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java index a641033b6ce1..e078a66d110a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java @@ -89,7 +89,7 @@ protected ModelAndView writeToInternal(HttpServletRequest request, HttpServletRe result = new DeferredResult<>(); } - AsyncServerResponse.writeAsync(request, response, result); + DefaultAsyncServerResponse.writeAsync(request, response, result); this.sseConsumer.accept(new DefaultSseBuilder(response, context, result)); return null; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java new file mode 100644 index 000000000000..8f3cf3546d9d --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultAsyncServerResponseTests.java @@ -0,0 +1,39 @@ +/* + * 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.function; + +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class DefaultAsyncServerResponseTests { + + @Test + void block() { + ServerResponse wrappee = ServerResponse.ok().build(); + CompletableFuture future = CompletableFuture.completedFuture(wrappee); + AsyncServerResponse response = AsyncServerResponse.create(future); + + assertThat(response.block()).isSameAs(wrappee); + } + +} From 4337d8465cded29b43aa32946bf691af48f34043 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 3 Dec 2020 14:41:23 +0100 Subject: [PATCH 0094/1294] Remove spring.event.invoke-listener startup event Prior to this commit, the `SimpleApplicationEventMulticaster` would be instrumented with the `ApplicationStartup` and start/stop events for invoking event listeners (`spring.event.invoke-listener`). This feature was already limited to single-threaded event publishers, but is still flawed since several types of events can happen concurrently. Due to the single-threaded nature of the startup sequence, our implementation should not produce startup events concurrently. This can cause issues like gh-26057, where concurrent events lead to inconcistencies when tracking parent/child relationships. This commit removes the `spring.event.invoke-listener` startup event as a result. Fixes gh-26057 --- .../SimpleApplicationEventMulticaster.java | 31 ------------------- .../support/AbstractApplicationContext.java | 4 +-- src/docs/asciidoc/core/core-appendix.adoc | 4 --- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java index 503efacc4f11..2ca8e8d092dc 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -25,8 +25,6 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.ResolvableType; -import org.springframework.core.metrics.ApplicationStartup; -import org.springframework.core.metrics.StartupStep; import org.springframework.lang.Nullable; import org.springframework.util.ErrorHandler; @@ -57,9 +55,6 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM @Nullable private ErrorHandler errorHandler; - @Nullable - private ApplicationStartup applicationStartup; - /** * Create a new SimpleApplicationEventMulticaster. @@ -127,22 +122,6 @@ protected ErrorHandler getErrorHandler() { return this.errorHandler; } - /** - * Set the {@link ApplicationStartup} to track event listener invocations during startup. - * @since 5.3 - */ - public void setApplicationStartup(@Nullable ApplicationStartup applicationStartup) { - this.applicationStartup = applicationStartup; - } - - /** - * Return the current application startup for this multicaster. - */ - @Nullable - public ApplicationStartup getApplicationStartup() { - return this.applicationStartup; - } - @Override public void multicastEvent(ApplicationEvent event) { multicastEvent(event, resolveDefaultEventType(event)); @@ -156,16 +135,6 @@ public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableTyp if (executor != null) { executor.execute(() -> invokeListener(listener, event)); } - else if (this.applicationStartup != null) { - StartupStep invocationStep = this.applicationStartup.start("spring.event.invoke-listener"); - invokeListener(listener, event); - invocationStep.tag("event", event::toString); - if (eventType != null) { - invocationStep.tag("eventType", eventType::toString); - } - invocationStep.tag("listener", listener::toString); - invocationStep.end(); - } else { invokeListener(listener, event); } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 2a2aa818e2b9..e87edab5f626 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -815,9 +815,7 @@ protected void initApplicationEventMulticaster() { } } else { - SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); - simpleApplicationEventMulticaster.setApplicationStartup(getApplicationStartup()); - this.applicationEventMulticaster = simpleApplicationEventMulticaster; + this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster); if (logger.isTraceEnabled()) { logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " + diff --git a/src/docs/asciidoc/core/core-appendix.adoc b/src/docs/asciidoc/core/core-appendix.adoc index 877b9b1bfbf0..7d3645fba1ac 100644 --- a/src/docs/asciidoc/core/core-appendix.adoc +++ b/src/docs/asciidoc/core/core-appendix.adoc @@ -1669,8 +1669,4 @@ its behavior changes. | `spring.context.refresh` | Application context refresh phase. | - -| `spring.event.invoke-listener` -| Invocation of event listeners, if done in the main thread. -| `event` the current application event, `eventType` its type and `listener` the listener processing this event. |=== \ No newline at end of file From 9776929a9d0d0e9bed98df32bf465b756e15b056 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 3 Dec 2020 15:33:37 +0100 Subject: [PATCH 0095/1294] Mention security considerations in Forwarded filters This commit improves the Javadoc for the `ForwardedHeaderFilter` (Servlet Filter) and `ForwardedHeaderTransformer` (reactive variant) so as to mention security considerations linked to Forwarded HTTP headers. Closes gh-26081 --- .../web/filter/ForwardedHeaderFilter.java | 9 +++++++-- .../server/adapter/ForwardedHeaderTransformer.java | 11 ++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java index 40d74ae24f7e..4a4938b49748 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java @@ -56,8 +56,13 @@ *
  • {@link HttpServletResponse#sendRedirect(String) sendRedirect(String)}. * * - *

    This filter can also be used in a {@link #setRemoveOnly removeOnly} mode - * where "Forwarded" and "X-Forwarded-*" headers are eliminated, and not used. + *

    There are security considerations for forwarded headers since an application + * cannot know if the headers were added by a proxy, as intended, or by a malicious + * client. This is why a proxy at the boundary of trust should be configured to + * remove untrusted Forwarded headers that come from the outside. + * + *

    You can also configure the ForwardedHeaderFilter with {@link #setRemoveOnly removeOnly}, + * in which case it removes but does not use the headers. * * @author Rossen Stoyanchev * @author Eddú Meléndez diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java index 531d73413563..fac2f1acfad8 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java @@ -36,15 +36,20 @@ * the request URI (i.e. {@link ServerHttpRequest#getURI()}) so it reflects * the client-originated protocol and address. * - *

    Alternatively if {@link #setRemoveOnly removeOnly} is set to "true", - * then "Forwarded" and "X-Forwarded-*" headers are only removed, and not used. - * *

    An instance of this class is typically declared as a bean with the name * "forwardedHeaderTransformer" and detected by * {@link WebHttpHandlerBuilder#applicationContext(ApplicationContext)}, or it * can also be registered directly via * {@link WebHttpHandlerBuilder#forwardedHeaderTransformer(ForwardedHeaderTransformer)}. * + *

    There are security considerations for forwarded headers since an application + * cannot know if the headers were added by a proxy, as intended, or by a malicious + * client. This is why a proxy at the boundary of trust should be configured to + * remove untrusted Forwarded headers that come from the outside. + * + *

    You can also configure the ForwardedHeaderFilter with {@link #setRemoveOnly removeOnly}, + * in which case it removes but does not use the headers. + * * @author Rossen Stoyanchev * @since 5.1 * @see https://tools.ietf.org/html/rfc7239 From 01892c6524c7d506b5531419233a5d29c6d84dff Mon Sep 17 00:00:00 2001 From: shevtsiv Date: Fri, 16 Oct 2020 23:56:33 +0300 Subject: [PATCH 0096/1294] Optimization in ResourceArrayPropertyEditor The previous implementation uses ArrayList for storing resolved resources and ArrayList has O(n) time complexity for the contains operation. By switching to the HashSet for storing resolved resources we improve the time complexity of this operation to be O(1). See gh-25927 --- .../io/support/ResourceArrayPropertyEditor.java | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java index b7680648dc14..75e6754bee8b 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java @@ -18,10 +18,10 @@ import java.beans.PropertyEditorSupport; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.List; +import java.util.HashSet; +import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -129,7 +129,7 @@ public void setAsText(String text) { public void setValue(Object value) throws IllegalArgumentException { if (value instanceof Collection || (value instanceof Object[] && !(value instanceof Resource[]))) { Collection input = (value instanceof Collection ? (Collection) value : Arrays.asList((Object[]) value)); - List merged = new ArrayList<>(); + Set merged = new HashSet<>(input.size()); for (Object element : input) { if (element instanceof String) { // A location pattern: resolve it into a Resource array. @@ -137,11 +137,7 @@ public void setValue(Object value) throws IllegalArgumentException { String pattern = resolvePath((String) element).trim(); try { Resource[] resources = this.resourcePatternResolver.getResources(pattern); - for (Resource resource : resources) { - if (!merged.contains(resource)) { - merged.add(resource); - } - } + merged.addAll(Arrays.asList(resources)); } catch (IOException ex) { // ignore - might be an unresolved placeholder or non-existing base directory @@ -152,10 +148,7 @@ public void setValue(Object value) throws IllegalArgumentException { } else if (element instanceof Resource) { // A Resource object: add it to the result. - Resource resource = (Resource) element; - if (!merged.contains(resource)) { - merged.add(resource); - } + merged.add((Resource) element); } else { throw new IllegalArgumentException("Cannot convert element [" + element + "] to [" + From 8c1d06e0c42b0b6da30ea4f62b0c6988f360c43f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 3 Dec 2020 17:15:18 +0000 Subject: [PATCH 0097/1294] Polishing contribution Closes gh-25927 --- .../core/io/support/ResourceArrayPropertyEditor.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java index 75e6754bee8b..ec1c00c599bd 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java @@ -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. @@ -20,7 +20,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Set; import org.apache.commons.logging.Log; @@ -129,7 +130,7 @@ public void setAsText(String text) { public void setValue(Object value) throws IllegalArgumentException { if (value instanceof Collection || (value instanceof Object[] && !(value instanceof Resource[]))) { Collection input = (value instanceof Collection ? (Collection) value : Arrays.asList((Object[]) value)); - Set merged = new HashSet<>(input.size()); + Set merged = new LinkedHashSet<>(); for (Object element : input) { if (element instanceof String) { // A location pattern: resolve it into a Resource array. @@ -137,7 +138,7 @@ public void setValue(Object value) throws IllegalArgumentException { String pattern = resolvePath((String) element).trim(); try { Resource[] resources = this.resourcePatternResolver.getResources(pattern); - merged.addAll(Arrays.asList(resources)); + Collections.addAll(merged, resources); } catch (IOException ex) { // ignore - might be an unresolved placeholder or non-existing base directory From d82cb15439c1c9d4100daabb275e0eb102a877bc Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 3 Dec 2020 17:53:11 +0000 Subject: [PATCH 0098/1294] Ensure both mock files and parts can be provided Closes gh-26166 --- ...ockMultipartHttpServletRequestBuilder.java | 24 +++++++++++++++-- ...ltipartHttpServletRequestBuilderTests.java | 26 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java index dea33261c227..75cb972331be 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java @@ -16,6 +16,7 @@ package org.springframework.test.web.servlet.request; +import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -24,12 +25,14 @@ import javax.servlet.ServletContext; import javax.servlet.http.Part; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.mock.web.MockPart; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -119,7 +122,7 @@ public Object merge(@Nullable Object parent) { if (parent instanceof MockMultipartHttpServletRequestBuilder) { MockMultipartHttpServletRequestBuilder parentBuilder = (MockMultipartHttpServletRequestBuilder) parent; this.files.addAll(parentBuilder.files); - parentBuilder.parts.keySet().stream().forEach(name -> + parentBuilder.parts.keySet().forEach(name -> this.parts.putIfAbsent(name, parentBuilder.parts.get(name))); } @@ -138,9 +141,26 @@ public Object merge(@Nullable Object parent) { @Override protected final MockHttpServletRequest createServletRequest(ServletContext servletContext) { MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(servletContext); - this.files.forEach(request::addFile); + this.files.forEach(file -> request.addPart(toMockPart(file))); this.parts.values().stream().flatMap(Collection::stream).forEach(request::addPart); return request; } + private MockPart toMockPart(MockMultipartFile file) { + byte[] bytes = null; + if (!file.isEmpty()) { + try { + bytes = file.getBytes(); + } + catch (IOException ex) { + throw new IllegalStateException("Unexpected IOException", ex); + } + } + MockPart part = new MockPart(file.getName(), file.getOriginalFilename(), bytes); + if (file.getContentType() != null) { + part.getHeaders().set(HttpHeaders.CONTENT_TYPE, file.getContentType()); + } + return part; + } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilderTests.java index 82f039d590bc..a7087f424fb0 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -16,21 +16,43 @@ package org.springframework.test.web.servlet.request; +import java.nio.charset.StandardCharsets; + +import javax.servlet.http.Part; + import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockPart; import org.springframework.mock.web.MockServletContext; +import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; import static org.assertj.core.api.Assertions.assertThat; /** + * Unit tests for {@link MockMultipartHttpServletRequestBuilder}. * @author Rossen Stoyanchev */ public class MockMultipartHttpServletRequestBuilderTests { + @Test // gh-26166 + void addFilesAndParts() throws Exception { + MockHttpServletRequest mockRequest = new MockMultipartHttpServletRequestBuilder("/upload") + .file(new MockMultipartFile("file", "test.txt", "text/plain", "Test".getBytes(StandardCharsets.UTF_8))) + .part(new MockPart("data", "{\"node\":\"node\"}".getBytes(StandardCharsets.UTF_8))) + .buildRequest(new MockServletContext()); + + StandardMultipartHttpServletRequest parsedRequest = new StandardMultipartHttpServletRequest(mockRequest); + + assertThat(parsedRequest.getParameterMap()).containsOnlyKeys("data"); + assertThat(parsedRequest.getFileMap()).containsOnlyKeys("file"); + assertThat(parsedRequest.getParts()).extracting(Part::getName).containsExactly("file", "data"); + } + @Test - public void test() { + void mergeAndBuild() { MockHttpServletRequestBuilder parent = new MockHttpServletRequestBuilder(HttpMethod.GET, "/"); parent.characterEncoding("UTF-8"); Object result = new MockMultipartHttpServletRequestBuilder("/fileUpload").merge(parent); From 8ac39a50feda71194e33a456c0f8207169a5a3a9 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 3 Dec 2020 18:41:18 +0000 Subject: [PATCH 0099/1294] ServletServerHttpResponse reflects Content-Type override Closes gh-25490 --- .../server/ServletServerHttpResponse.java | 19 ++++++++---- .../ServletServerHttpResponseTests.java | 30 ++++++++++++------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java index 45758ec0d9aa..0e6b46a1e2dd 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java @@ -20,6 +20,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletResponse; @@ -152,12 +153,14 @@ public boolean containsKey(Object key) { @Override @Nullable public String getFirst(String headerName) { - String value = servletResponse.getHeader(headerName); - if (value != null) { - return value; + if (headerName.equalsIgnoreCase(CONTENT_TYPE)) { + // Content-Type is written as an override so check super first + String value = super.getFirst(headerName); + return (value != null ? value : servletResponse.getHeader(headerName)); } else { - return super.getFirst(headerName); + String value = servletResponse.getHeader(headerName); + return (value != null ? value : super.getFirst(headerName)); } } @@ -165,7 +168,13 @@ public String getFirst(String headerName) { public List get(Object key) { Assert.isInstanceOf(String.class, key, "Key must be a String-based header name"); - Collection values1 = servletResponse.getHeaders((String) key); + String headerName = (String) key; + if (headerName.equalsIgnoreCase(CONTENT_TYPE)) { + // Content-Type is written as an override so don't merge + return Collections.singletonList(getFirst(headerName)); + } + + Collection values1 = servletResponse.getHeaders(headerName); if (headersWritten) { return new ArrayList<>(values1); } diff --git a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java index 4f37e8d193ba..3dbef26f1b3d 100644 --- a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -17,7 +17,6 @@ package org.springframework.http.server; import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -44,20 +43,20 @@ public class ServletServerHttpResponseTests { @BeforeEach - public void create() throws Exception { + void create() { mockResponse = new MockHttpServletResponse(); response = new ServletServerHttpResponse(mockResponse); } @Test - public void setStatusCode() throws Exception { + void setStatusCode() { response.setStatusCode(HttpStatus.NOT_FOUND); assertThat(mockResponse.getStatus()).as("Invalid status code").isEqualTo(404); } @Test - public void getHeaders() throws Exception { + void getHeaders() { HttpHeaders headers = response.getHeaders(); String headerName = "MyHeader"; String headerValue1 = "value1"; @@ -77,23 +76,32 @@ public void getHeaders() throws Exception { } @Test - public void preExistingHeadersFromHttpServletResponse() { + void preExistingHeadersFromHttpServletResponse() { String headerName = "Access-Control-Allow-Origin"; String headerValue = "localhost:8080"; this.mockResponse.addHeader(headerName, headerValue); + this.mockResponse.setContentType("text/csv"); this.response = new ServletServerHttpResponse(this.mockResponse); assertThat(this.response.getHeaders().getFirst(headerName)).isEqualTo(headerValue); - assertThat(this.response.getHeaders().get(headerName)).isEqualTo(Collections.singletonList(headerValue)); - assertThat(this.response.getHeaders().containsKey(headerName)).isTrue(); - assertThat(this.response.getHeaders().getFirst(headerName)).isEqualTo(headerValue); + assertThat(this.response.getHeaders().get(headerName)).containsExactly(headerValue); + assertThat(this.response.getHeaders()).containsKey(headerName); assertThat(this.response.getHeaders().getAccessControlAllowOrigin()).isEqualTo(headerValue); } + @Test // gh-25490 + void preExistingContentTypeIsOverriddenImmediately() { + this.mockResponse.setContentType("text/csv"); + this.response = new ServletServerHttpResponse(this.mockResponse); + this.response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + + assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + } + @Test - public void getBody() throws Exception { - byte[] content = "Hello World".getBytes("UTF-8"); + void getBody() throws Exception { + byte[] content = "Hello World".getBytes(StandardCharsets.UTF_8); FileCopyUtils.copy(content, response.getBody()); assertThat(mockResponse.getContentAsByteArray()).as("Invalid content written").isEqualTo(content); From ef05eb37294ffd4ae628aca39bfc597f9b7c8158 Mon Sep 17 00:00:00 2001 From: limo520 Date: Fri, 4 Dec 2020 20:59:16 +0800 Subject: [PATCH 0100/1294] Fix assertions in SpelParserTests.generalExpressions Closes gh-26211 Co-authored-by: fengyuanwei --- .../expression/spel/standard/SpelParserTests.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java index f2a37e55ede5..89da320c3b58 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java @@ -117,37 +117,37 @@ public void generalExpressions() { SpelExpressionParser parser = new SpelExpressionParser(); parser.parseRaw("new String"); }) - .satisfies(ex -> parseExceptionRequirements(SpelMessage.MISSING_CONSTRUCTOR_ARGS, 10)); + .satisfies(parseExceptionRequirements(SpelMessage.MISSING_CONSTRUCTOR_ARGS, 10)); assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { SpelExpressionParser parser = new SpelExpressionParser(); parser.parseRaw("new String(3,"); }) - .satisfies(ex -> parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); + .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { SpelExpressionParser parser = new SpelExpressionParser(); parser.parseRaw("new String(3"); }) - .satisfies(ex -> parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); + .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { SpelExpressionParser parser = new SpelExpressionParser(); parser.parseRaw("new String("); }) - .satisfies(ex -> parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); + .satisfies(parseExceptionRequirements(SpelMessage.RUN_OUT_OF_ARGUMENTS, 10)); assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { SpelExpressionParser parser = new SpelExpressionParser(); parser.parseRaw("\"abc"); }) - .satisfies(ex -> parseExceptionRequirements(SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING, 0)); + .satisfies(parseExceptionRequirements(SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING, 0)); assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { SpelExpressionParser parser = new SpelExpressionParser(); parser.parseRaw("'abc"); }) - .satisfies(ex -> parseExceptionRequirements(SpelMessage.NON_TERMINATING_QUOTED_STRING, 0)); + .satisfies(parseExceptionRequirements(SpelMessage.NON_TERMINATING_QUOTED_STRING, 0)); } From ec7425c1f4b5024a87d9af6de2a19eb29727cc24 Mon Sep 17 00:00:00 2001 From: VonUniGE Date: Wed, 2 Dec 2020 15:04:57 +0100 Subject: [PATCH 0101/1294] Fix typo in javadoc for ResponseExtractor --- .../java/org/springframework/web/client/ResponseExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/ResponseExtractor.java b/spring-web/src/main/java/org/springframework/web/client/ResponseExtractor.java index e36bb0715634..c20b0e9c8877 100644 --- a/spring-web/src/main/java/org/springframework/web/client/ResponseExtractor.java +++ b/spring-web/src/main/java/org/springframework/web/client/ResponseExtractor.java @@ -23,7 +23,7 @@ import org.springframework.lang.Nullable; /** - * Generic callback interface used by {@link RestTemplate}'s retrieval methods + * Generic callback interface used by {@link RestTemplate}'s retrieval methods. * Implementations of this interface perform the actual work of extracting data * from a {@link ClientHttpResponse}, but don't need to worry about exception * handling or closing resources. From 7b3f53de506b1a022ded8f061cd2ae3f30c3c2f1 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 4 Dec 2020 14:05:30 +0100 Subject: [PATCH 0102/1294] Polish SpelParserTests --- .../spel/standard/SpelParserTests.java | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java index 89da320c3b58..9133b7baf003 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/standard/SpelParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -36,10 +36,10 @@ * @author Andy Clement * @author Juergen Hoeller */ -public class SpelParserTests { +class SpelParserTests { @Test - public void theMostBasic() { + void theMostBasic() { SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2"); assertThat(expr).isNotNull(); @@ -50,7 +50,7 @@ public void theMostBasic() { } @Test - public void valueType() { + void valueType() { SpelExpressionParser parser = new SpelExpressionParser(); EvaluationContext ctx = new StandardEvaluationContext(); Class c = parser.parseRaw("2").getValueType(); @@ -66,7 +66,7 @@ public void valueType() { } @Test - public void whitespace() { + void whitespace() { SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2 + 3"); assertThat(expr.getValue()).isEqualTo(5); @@ -79,7 +79,7 @@ public void whitespace() { } @Test - public void arithmeticPlus1() { + void arithmeticPlus1() { SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2+2"); assertThat(expr).isNotNull(); @@ -88,31 +88,30 @@ public void arithmeticPlus1() { } @Test - public void arithmeticPlus2() { + void arithmeticPlus2() { SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("37+41"); assertThat(expr.getValue()).isEqualTo(78); } @Test - public void arithmeticMultiply1() { + void arithmeticMultiply1() { SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2*3"); assertThat(expr).isNotNull(); assertThat(expr.getAST()).isNotNull(); - // printAst(expr.getAST(),0); assertThat(expr.getValue()).isEqualTo(6); } @Test - public void arithmeticPrecedence1() { + void arithmeticPrecedence1() { SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2*3+5"); assertThat(expr.getValue()).isEqualTo(11); } @Test - public void generalExpressions() { + void generalExpressions() { assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> { SpelExpressionParser parser = new SpelExpressionParser(); parser.parseRaw("new String"); @@ -161,38 +160,38 @@ private Consumer parseExceptionRequirements( } @Test - public void arithmeticPrecedence2() { + void arithmeticPrecedence2() { SpelExpressionParser parser = new SpelExpressionParser(); SpelExpression expr = parser.parseRaw("2+3*5"); assertThat(expr.getValue()).isEqualTo(17); } @Test - public void arithmeticPrecedence3() { + void arithmeticPrecedence3() { SpelExpression expr = new SpelExpressionParser().parseRaw("3+10/2"); assertThat(expr.getValue()).isEqualTo(8); } @Test - public void arithmeticPrecedence4() { + void arithmeticPrecedence4() { SpelExpression expr = new SpelExpressionParser().parseRaw("10/2+3"); assertThat(expr.getValue()).isEqualTo(8); } @Test - public void arithmeticPrecedence5() { + void arithmeticPrecedence5() { SpelExpression expr = new SpelExpressionParser().parseRaw("(4+10)/2"); assertThat(expr.getValue()).isEqualTo(7); } @Test - public void arithmeticPrecedence6() { + void arithmeticPrecedence6() { SpelExpression expr = new SpelExpressionParser().parseRaw("(3+2)*2"); assertThat(expr.getValue()).isEqualTo(10); } @Test - public void booleanOperators() { + void booleanOperators() { SpelExpression expr = new SpelExpressionParser().parseRaw("true"); assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); expr = new SpelExpressionParser().parseRaw("false"); @@ -210,7 +209,7 @@ public void booleanOperators() { } @Test - public void booleanOperators_symbolic_spr9614() { + void booleanOperators_symbolic_spr9614() { SpelExpression expr = new SpelExpressionParser().parseRaw("true"); assertThat(expr.getValue(Boolean.class)).isEqualTo(Boolean.TRUE); expr = new SpelExpressionParser().parseRaw("false"); @@ -228,7 +227,7 @@ public void booleanOperators_symbolic_spr9614() { } @Test - public void stringLiterals() { + void stringLiterals() { SpelExpression expr = new SpelExpressionParser().parseRaw("'howdy'"); assertThat(expr.getValue()).isEqualTo("howdy"); expr = new SpelExpressionParser().parseRaw("'hello '' world'"); @@ -236,13 +235,13 @@ public void stringLiterals() { } @Test - public void stringLiterals2() { + void stringLiterals2() { SpelExpression expr = new SpelExpressionParser().parseRaw("'howdy'.substring(0,2)"); assertThat(expr.getValue()).isEqualTo("ho"); } @Test - public void testStringLiterals_DoubleQuotes_spr9620() { + void testStringLiterals_DoubleQuotes_spr9620() { SpelExpression expr = new SpelExpressionParser().parseRaw("\"double quote: \"\".\""); assertThat(expr.getValue()).isEqualTo("double quote: \"."); expr = new SpelExpressionParser().parseRaw("\"hello \"\" world\""); @@ -250,7 +249,7 @@ public void testStringLiterals_DoubleQuotes_spr9620() { } @Test - public void testStringLiterals_DoubleQuotes_spr9620_2() { + void testStringLiterals_DoubleQuotes_spr9620_2() { assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> new SpelExpressionParser().parseRaw("\"double quote: \\\"\\\".\"")) .satisfies(ex -> { @@ -260,7 +259,7 @@ public void testStringLiterals_DoubleQuotes_spr9620_2() { } @Test - public void positionalInformation() { + void positionalInformation() { SpelExpression expr = new SpelExpressionParser().parseRaw("true and true or false"); SpelNode rootAst = expr.getAST(); OpOr operatorOr = (OpOr) rootAst; @@ -289,7 +288,7 @@ public void positionalInformation() { } @Test - public void tokenKind() { + void tokenKind() { TokenKind tk = TokenKind.NOT; assertThat(tk.hasPayload()).isFalse(); assertThat(tk.toString()).isEqualTo("NOT(!)"); @@ -304,7 +303,7 @@ public void tokenKind() { } @Test - public void token() { + void token() { Token token = new Token(TokenKind.NOT, 0, 3); assertThat(token.kind).isEqualTo(TokenKind.NOT); assertThat(token.startPos).isEqualTo(0); @@ -319,7 +318,7 @@ public void token() { } @Test - public void exceptions() { + void exceptions() { ExpressionException exprEx = new ExpressionException("test"); assertThat(exprEx.getSimpleMessage()).isEqualTo("test"); assertThat(exprEx.toDetailedString()).isEqualTo("test"); @@ -337,13 +336,13 @@ public void exceptions() { } @Test - public void parseMethodsOnNumbers() { + void parseMethodsOnNumbers() { checkNumber("3.14.toString()", "3.14", String.class); checkNumber("3.toString()", "3", String.class); } @Test - public void numerics() { + void numerics() { checkNumber("2", 2, Integer.class); checkNumber("22", 22, Integer.class); checkNumber("+22", 22, Integer.class); @@ -385,8 +384,7 @@ private void checkNumber(String expression, Object value, Class type) { private void checkNumberError(String expression, SpelMessage expectedMessage) { SpelExpressionParser parser = new SpelExpressionParser(); - assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> - parser.parseRaw(expression)) + assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> parser.parseRaw(expression)) .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(expectedMessage)); } From 3c85c263d2b088c9b84a701e9e711001b17305dd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 4 Dec 2020 23:29:21 +0100 Subject: [PATCH 0103/1294] Upgrade to Groovy 3.0.7, RxJava 3.0.8, Mockito 3.6.28 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 3a9bfdfdb167..69ec9eacc38d 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,7 @@ configure(allprojects) { project -> entry 'aspectjtools' entry 'aspectjweaver' } - dependencySet(group: 'org.codehaus.groovy', version: '3.0.6') { + dependencySet(group: 'org.codehaus.groovy', version: '3.0.7') { entry 'groovy' entry 'groovy-jsr223' entry 'groovy-templates' // requires findbugs for warning-free compilation @@ -64,7 +64,7 @@ configure(allprojects) { project -> dependency "io.reactivex:rxjava:1.3.8" dependency "io.reactivex:rxjava-reactive-streams:1.2.1" dependency "io.reactivex.rxjava2:rxjava:2.2.19" - dependency "io.reactivex.rxjava3:rxjava:3.0.7" + dependency "io.reactivex.rxjava3:rxjava:3.0.8" dependency "io.projectreactor.tools:blockhound:1.0.4.RELEASE" dependency "com.caucho:hessian:4.0.63" @@ -197,7 +197,7 @@ configure(allprojects) { project -> exclude group: "org.hamcrest", name: "hamcrest-core" } } - dependencySet(group: 'org.mockito', version: '3.6.0') { + dependencySet(group: 'org.mockito', version: '3.6.28') { entry('mockito-core') { exclude group: "org.hamcrest", name: "hamcrest-core" } From a1d134b0867b52fd2defae706e4b7c4d82b60165 Mon Sep 17 00:00:00 2001 From: mplain Date: Wed, 4 Nov 2020 12:08:56 +0300 Subject: [PATCH 0104/1294] Add missing Kotlin extensions for WebClient.ResponseSpec Closes gh-26030 --- .../function/client/WebClientExtensions.kt | 31 +++++++++++++++++++ .../client/WebClientExtensionsTests.kt | 18 +++++++++++ 2 files changed, 49 insertions(+) diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt index b773e06e302b..12b13e920bda 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/client/WebClientExtensions.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.reactor.asFlux import kotlinx.coroutines.reactor.mono import org.reactivestreams.Publisher import org.springframework.core.ParameterizedTypeReference +import org.springframework.http.ResponseEntity import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec import reactor.core.publisher.Flux @@ -138,3 +139,33 @@ inline fun WebClient.ResponseSpec.bodyToFlow(): Flow = */ suspend inline fun WebClient.ResponseSpec.awaitBody() : T = bodyToMono().awaitSingle() + +/** + * Extension for [WebClient.ResponseSpec.toEntity] providing a `toEntity()` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * + * @since 5.3.2 + */ +inline fun WebClient.ResponseSpec.toEntity(): Mono> = + toEntity(object : ParameterizedTypeReference() {}) + +/** + * Extension for [WebClient.ResponseSpec.toEntityList] providing a `toEntityList()` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * + * @since 5.3.2 + */ +inline fun WebClient.ResponseSpec.toEntityList(): Mono>> = + toEntityList(object : ParameterizedTypeReference() {}) + +/** + * Extension for [WebClient.ResponseSpec.toEntityFlux] providing a `toEntityFlux()` variant + * leveraging Kotlin reified type parameters. This extension is not subject to type + * erasure and retains actual generic type arguments. + * + * @since 5.3.2 + */ +inline fun WebClient.ResponseSpec.toEntityFlux(): Mono>> = + toEntityFlux(object : ParameterizedTypeReference() {}) diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt index cff9e8edce63..f9ace0e04b76 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/client/WebClientExtensionsTests.kt @@ -125,5 +125,23 @@ class WebClientExtensionsTests { } } + @Test + fun `ResponseSpec#toEntity with reified type parameters`() { + responseSpec.toEntity>() + verify { responseSpec.toEntity(object : ParameterizedTypeReference>() {}) } + } + + @Test + fun `ResponseSpec#toEntityList with reified type parameters`() { + responseSpec.toEntityList>() + verify { responseSpec.toEntityList(object : ParameterizedTypeReference>() {}) } + } + + @Test + fun `ResponseSpec#toEntityFlux with reified type parameters`() { + responseSpec.toEntityFlux>() + verify { responseSpec.toEntityFlux(object : ParameterizedTypeReference>() {}) } + } + class Foo } From da15fe4c9423e7e17f40838d9519385a47f23a16 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 7 Dec 2020 10:43:22 +0100 Subject: [PATCH 0105/1294] Upgrade to Gradle 6.7.1 Closes gh-26225 --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index be52383ef49c..4d9ca1649142 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 994ec708fc076792daa4012538f3e6a4609a1147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 7 Dec 2020 10:48:10 +0100 Subject: [PATCH 0106/1294] Upgrade to Kotlin Coroutines 1.4.2 Closes gh-26226 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 69ec9eacc38d..ac605cca33f5 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ configure(allprojects) { project -> mavenBom "io.rsocket:rsocket-bom:1.1.0" mavenBom "org.eclipse.jetty:jetty-bom:9.4.35.v20201120" mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.20" - mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.1" + mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.2" mavenBom "org.junit:junit-bom:5.7.0" } dependencies { From 017242463502f451c6c71a823b9c5232276dd78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 7 Dec 2020 09:57:26 +0100 Subject: [PATCH 0107/1294] Avoid CGLIB proxies on websocket/messaging configurations This commit updates websocket and messaging configurations in order to not use CGLIB proxies anymore. The goal here is to allow support in native executables and to increase the consistency across the portfolio. Closes gh-26227 --- .../AbstractMessageBrokerConfiguration.java | 145 ++++++++++-------- .../MessageBrokerConfigurationTests.java | 17 +- .../DelegatingWebSocketConfiguration.java | 5 +- ...ngWebSocketMessageBrokerConfiguration.java | 5 +- .../WebSocketConfigurationSupport.java | 7 +- ...cketMessageBrokerConfigurationSupport.java | 47 +++--- ...essageBrokerConfigurationSupportTests.java | 15 +- .../StompWebSocketIntegrationTests.java | 6 +- 8 files changed, 144 insertions(+), 103 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java index 04db543c23d6..9ec81f49778e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java @@ -71,18 +71,18 @@ * Provides essential configuration for handling messages with simple messaging * protocols such as STOMP. * - *

    {@link #clientInboundChannel()} and {@link #clientOutboundChannel()} deliver + *

    {@link #clientInboundChannel(TaskExecutor)} and {@link #clientOutboundChannel(TaskExecutor)} deliver * messages to and from remote clients to several message handlers such as the * following. *

      - *
    • {@link #simpAnnotationMethodMessageHandler()}
    • - *
    • {@link #simpleBrokerMessageHandler()}
    • - *
    • {@link #stompBrokerRelayMessageHandler()}
    • - *
    • {@link #userDestinationMessageHandler()}
    • + *
    • {@link #simpAnnotationMethodMessageHandler(AbstractSubscribableChannel, AbstractSubscribableChannel, SimpMessagingTemplate, CompositeMessageConverter)}
    • + *
    • {@link #simpleBrokerMessageHandler(AbstractSubscribableChannel, AbstractSubscribableChannel, AbstractSubscribableChannel, UserDestinationResolver)}
    • + *
    • {@link #stompBrokerRelayMessageHandler(AbstractSubscribableChannel, AbstractSubscribableChannel, AbstractSubscribableChannel, UserDestinationMessageHandler, MessageHandler, UserDestinationResolver)}
    • + *
    • {@link #userDestinationMessageHandler(AbstractSubscribableChannel, AbstractSubscribableChannel, AbstractSubscribableChannel, UserDestinationResolver)}
    • *
    * - *

    {@link #brokerChannel()} delivers messages from within the application to the - * the respective message handlers. {@link #brokerMessagingTemplate()} can be injected + *

    {@link #brokerChannel(AbstractSubscribableChannel, AbstractSubscribableChannel, TaskExecutor)} delivers messages from within the application to the + * the respective message handlers. {@link #brokerMessagingTemplate(AbstractSubscribableChannel, AbstractSubscribableChannel, AbstractSubscribableChannel, CompositeMessageConverter)} can be injected * into any application component to send messages. * *

    Subclasses are responsible for the parts of the configuration that feed messages @@ -90,6 +90,7 @@ * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Sebastien Deleuze * @since 4.0 */ public abstract class AbstractMessageBrokerConfiguration implements ApplicationContextAware { @@ -147,8 +148,8 @@ public ApplicationContext getApplicationContext() { @Bean - public AbstractSubscribableChannel clientInboundChannel() { - ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(clientInboundChannelExecutor()); + public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInboundChannelExecutor) { + ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(clientInboundChannelExecutor); channel.setLogger(SimpLogging.forLog(channel.getLogger())); ChannelRegistration reg = getClientInboundChannelRegistration(); if (reg.hasInterceptors()) { @@ -183,8 +184,8 @@ protected void configureClientInboundChannel(ChannelRegistration registration) { } @Bean - public AbstractSubscribableChannel clientOutboundChannel() { - ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(clientOutboundChannelExecutor()); + public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutboundChannelExecutor) { + ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel(clientOutboundChannelExecutor); channel.setLogger(SimpLogging.forLog(channel.getLogger())); ChannelRegistration reg = getClientOutboundChannelRegistration(); if (reg.hasInterceptors()) { @@ -219,10 +220,11 @@ protected void configureClientOutboundChannel(ChannelRegistration registration) } @Bean - public AbstractSubscribableChannel brokerChannel() { - ChannelRegistration reg = getBrokerRegistry().getBrokerChannelRegistration(); + public AbstractSubscribableChannel brokerChannel(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel, TaskExecutor brokerChannelExecutor) { + ChannelRegistration reg = getBrokerRegistry(clientInboundChannel, clientOutboundChannel).getBrokerChannelRegistration(); ExecutorSubscribableChannel channel = (reg.hasTaskExecutor() ? - new ExecutorSubscribableChannel(brokerChannelExecutor()) : new ExecutorSubscribableChannel()); + new ExecutorSubscribableChannel(brokerChannelExecutor) : new ExecutorSubscribableChannel()); reg.interceptors(new ImmutableMessageChannelInterceptor()); channel.setLogger(SimpLogging.forLog(channel.getLogger())); channel.setInterceptors(reg.getInterceptors()); @@ -230,8 +232,9 @@ public AbstractSubscribableChannel brokerChannel() { } @Bean - public TaskExecutor brokerChannelExecutor() { - ChannelRegistration reg = getBrokerRegistry().getBrokerChannelRegistration(); + public TaskExecutor brokerChannelExecutor(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel) { + ChannelRegistration reg = getBrokerRegistry(clientInboundChannel, clientOutboundChannel).getBrokerChannelRegistration(); ThreadPoolTaskExecutor executor; if (reg.hasTaskExecutor()) { executor = reg.taskExecutor().getTaskExecutor(); @@ -251,9 +254,10 @@ public TaskExecutor brokerChannelExecutor() { * An accessor for the {@link MessageBrokerRegistry} that ensures its one-time creation * and initialization through {@link #configureMessageBroker(MessageBrokerRegistry)}. */ - protected final MessageBrokerRegistry getBrokerRegistry() { + protected final MessageBrokerRegistry getBrokerRegistry(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel) { if (this.brokerRegistry == null) { - MessageBrokerRegistry registry = new MessageBrokerRegistry(clientInboundChannel(), clientOutboundChannel()); + MessageBrokerRegistry registry = new MessageBrokerRegistry(clientInboundChannel, clientOutboundChannel); configureMessageBroker(registry); this.brokerRegistry = registry; } @@ -272,15 +276,20 @@ protected void configureMessageBroker(MessageBrokerRegistry registry) { * configuration classes. */ @Nullable - public final PathMatcher getPathMatcher() { - return getBrokerRegistry().getPathMatcher(); + public final PathMatcher getPathMatcher(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel) { + return getBrokerRegistry(clientInboundChannel, clientOutboundChannel).getPathMatcher(); } @Bean - public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler() { - SimpAnnotationMethodMessageHandler handler = createAnnotationMethodMessageHandler(); - handler.setDestinationPrefixes(getBrokerRegistry().getApplicationDestinationPrefixes()); - handler.setMessageConverter(brokerMessageConverter()); + public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler( + AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel, + SimpMessagingTemplate brokerMessagingTemplate, CompositeMessageConverter brokerMessageConverter) { + SimpAnnotationMethodMessageHandler handler = createAnnotationMethodMessageHandler(clientInboundChannel, + clientOutboundChannel, brokerMessagingTemplate); + MessageBrokerRegistry brokerRegistry = getBrokerRegistry(clientInboundChannel, clientOutboundChannel); + handler.setDestinationPrefixes(brokerRegistry.getApplicationDestinationPrefixes()); + handler.setMessageConverter(brokerMessageConverter); handler.setValidator(simpValidator()); List argumentResolvers = new ArrayList<>(); @@ -291,7 +300,7 @@ public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler() { addReturnValueHandlers(returnValueHandlers); handler.setCustomReturnValueHandlers(returnValueHandlers); - PathMatcher pathMatcher = getBrokerRegistry().getPathMatcher(); + PathMatcher pathMatcher = brokerRegistry.getPathMatcher(); if (pathMatcher != null) { handler.setPathMatcher(pathMatcher); } @@ -302,11 +311,12 @@ public SimpAnnotationMethodMessageHandler simpAnnotationMethodMessageHandler() { * Protected method for plugging in a custom subclass of * {@link org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler * SimpAnnotationMethodMessageHandler}. - * @since 4.2 + * @since 5.3.2 */ - protected SimpAnnotationMethodMessageHandler createAnnotationMethodMessageHandler() { - return new SimpAnnotationMethodMessageHandler(clientInboundChannel(), - clientOutboundChannel(), brokerMessagingTemplate()); + protected SimpAnnotationMethodMessageHandler createAnnotationMethodMessageHandler( + AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel, + SimpMessagingTemplate brokerMessagingTemplate) { + return new SimpAnnotationMethodMessageHandler(clientInboundChannel, clientOutboundChannel, brokerMessagingTemplate); } protected void addArgumentResolvers(List argumentResolvers) { @@ -317,48 +327,56 @@ protected void addReturnValueHandlers(List retu @Bean @Nullable - public AbstractBrokerMessageHandler simpleBrokerMessageHandler() { - SimpleBrokerMessageHandler handler = getBrokerRegistry().getSimpleBroker(brokerChannel()); + public AbstractBrokerMessageHandler simpleBrokerMessageHandler(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel, AbstractSubscribableChannel brokerChannel, + UserDestinationResolver userDestinationResolver) { + SimpleBrokerMessageHandler handler = getBrokerRegistry(clientInboundChannel, clientOutboundChannel).getSimpleBroker(brokerChannel); if (handler == null) { return null; } - updateUserDestinationResolver(handler); + updateUserDestinationResolver(handler, userDestinationResolver); return handler; } - private void updateUserDestinationResolver(AbstractBrokerMessageHandler handler) { + private void updateUserDestinationResolver(AbstractBrokerMessageHandler handler, UserDestinationResolver userDestinationResolver) { Collection prefixes = handler.getDestinationPrefixes(); if (!prefixes.isEmpty() && !prefixes.iterator().next().startsWith("/")) { - ((DefaultUserDestinationResolver) userDestinationResolver()).setRemoveLeadingSlash(true); + ((DefaultUserDestinationResolver) userDestinationResolver).setRemoveLeadingSlash(true); } } @Bean @Nullable - public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler() { - StompBrokerRelayMessageHandler handler = getBrokerRegistry().getStompBrokerRelay(brokerChannel()); + public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel, AbstractSubscribableChannel brokerChannel, + UserDestinationMessageHandler userDestinationMessageHandler, @Nullable MessageHandler userRegistryMessageHandler, + UserDestinationResolver userDestinationResolver) { + MessageBrokerRegistry brokerRegistry = getBrokerRegistry(clientInboundChannel, clientOutboundChannel); + StompBrokerRelayMessageHandler handler = brokerRegistry.getStompBrokerRelay(brokerChannel); if (handler == null) { return null; } Map subscriptions = new HashMap<>(4); - String destination = getBrokerRegistry().getUserDestinationBroadcast(); + String destination = brokerRegistry.getUserDestinationBroadcast(); if (destination != null) { - subscriptions.put(destination, userDestinationMessageHandler()); + subscriptions.put(destination, userDestinationMessageHandler); } - destination = getBrokerRegistry().getUserRegistryBroadcast(); + destination = brokerRegistry.getUserRegistryBroadcast(); if (destination != null) { - subscriptions.put(destination, userRegistryMessageHandler()); + subscriptions.put(destination, userRegistryMessageHandler); } handler.setSystemSubscriptions(subscriptions); - updateUserDestinationResolver(handler); + updateUserDestinationResolver(handler, userDestinationResolver); return handler; } @Bean - public UserDestinationMessageHandler userDestinationMessageHandler() { - UserDestinationMessageHandler handler = new UserDestinationMessageHandler(clientInboundChannel(), - brokerChannel(), userDestinationResolver()); - String destination = getBrokerRegistry().getUserDestinationBroadcast(); + public UserDestinationMessageHandler userDestinationMessageHandler(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel, AbstractSubscribableChannel brokerChannel, + UserDestinationResolver userDestinationResolver) { + UserDestinationMessageHandler handler = new UserDestinationMessageHandler(clientInboundChannel, + brokerChannel, userDestinationResolver); + String destination = getBrokerRegistry(clientInboundChannel, clientOutboundChannel).getUserDestinationBroadcast(); if (destination != null) { handler.setBroadcastDestination(destination); } @@ -367,15 +385,17 @@ public UserDestinationMessageHandler userDestinationMessageHandler() { @Bean @Nullable - public MessageHandler userRegistryMessageHandler() { - if (getBrokerRegistry().getUserRegistryBroadcast() == null) { + public MessageHandler userRegistryMessageHandler(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel, SimpUserRegistry userRegistry, + SimpMessagingTemplate brokerMessagingTemplate, TaskScheduler messageBrokerTaskScheduler) { + MessageBrokerRegistry brokerRegistry = getBrokerRegistry(clientInboundChannel, clientOutboundChannel); + if (brokerRegistry.getUserRegistryBroadcast() == null) { return null; } - SimpUserRegistry userRegistry = userRegistry(); Assert.isInstanceOf(MultiServerUserRegistry.class, userRegistry, "MultiServerUserRegistry required"); return new UserRegistryMessageHandler((MultiServerUserRegistry) userRegistry, - brokerMessagingTemplate(), getBrokerRegistry().getUserRegistryBroadcast(), - messageBrokerTaskScheduler()); + brokerMessagingTemplate, brokerRegistry.getUserRegistryBroadcast(), + messageBrokerTaskScheduler); } // Expose alias for 4.1 compatibility @@ -389,13 +409,15 @@ public TaskScheduler messageBrokerTaskScheduler() { } @Bean - public SimpMessagingTemplate brokerMessagingTemplate() { - SimpMessagingTemplate template = new SimpMessagingTemplate(brokerChannel()); - String prefix = getBrokerRegistry().getUserDestinationPrefix(); + public SimpMessagingTemplate brokerMessagingTemplate(AbstractSubscribableChannel brokerChannel, + AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel, + CompositeMessageConverter brokerMessageConverter) { + SimpMessagingTemplate template = new SimpMessagingTemplate(brokerChannel); + String prefix = getBrokerRegistry(clientInboundChannel, clientOutboundChannel).getUserDestinationPrefix(); if (prefix != null) { template.setUserDestinationPrefix(prefix); } - template.setMessageConverter(brokerMessageConverter()); + template.setMessageConverter(brokerMessageConverter); return template; } @@ -441,9 +463,10 @@ protected boolean configureMessageConverters(List messageConve } @Bean - public UserDestinationResolver userDestinationResolver() { - DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userRegistry()); - String prefix = getBrokerRegistry().getUserDestinationPrefix(); + public UserDestinationResolver userDestinationResolver(SimpUserRegistry userRegistry, + AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel) { + DefaultUserDestinationResolver resolver = new DefaultUserDestinationResolver(userRegistry); + String prefix = getBrokerRegistry(clientInboundChannel, clientOutboundChannel).getUserDestinationPrefix(); if (prefix != null) { resolver.setUserDestinationPrefix(prefix); } @@ -452,12 +475,14 @@ public UserDestinationResolver userDestinationResolver() { @Bean @SuppressWarnings("deprecation") - public SimpUserRegistry userRegistry() { + public SimpUserRegistry userRegistry(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel) { SimpUserRegistry registry = createLocalUserRegistry(); + MessageBrokerRegistry brokerRegistry = getBrokerRegistry(clientInboundChannel, clientOutboundChannel); if (registry == null) { - registry = createLocalUserRegistry(getBrokerRegistry().getUserRegistryOrder()); + registry = createLocalUserRegistry(brokerRegistry.getUserRegistryOrder()); } - boolean broadcast = getBrokerRegistry().getUserRegistryBroadcast() != null; + boolean broadcast = brokerRegistry.getUserRegistryBroadcast() != null; return (broadcast ? new MultiServerUserRegistry(registry) : registry); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java index 779c1de0526e..ca454849a212 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/config/MessageBrokerConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -30,6 +30,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.Ordered; +import org.springframework.core.task.TaskExecutor; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -594,19 +595,20 @@ public TestController subscriptionController() { @Override @Bean - public AbstractSubscribableChannel clientInboundChannel() { + public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInboundChannelExecutor) { return new TestChannel(); } @Override @Bean - public AbstractSubscribableChannel clientOutboundChannel() { + public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutboundChannelExecutor) { return new TestChannel(); } @Override @Bean - public AbstractSubscribableChannel brokerChannel() { + public AbstractSubscribableChannel brokerChannel(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel, TaskExecutor brokerChannelExecutor) { return new TestChannel(); } } @@ -680,20 +682,21 @@ protected void configureMessageBroker(MessageBrokerRegistry registry) { @Override @Bean - public AbstractSubscribableChannel clientInboundChannel() { + public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInboundChannelExecutor) { // synchronous return new ExecutorSubscribableChannel(null); } @Override @Bean - public AbstractSubscribableChannel clientOutboundChannel() { + public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutboundChannelExecutor) { return new TestChannel(); } @Override @Bean - public AbstractSubscribableChannel brokerChannel() { + public AbstractSubscribableChannel brokerChannel(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel, TaskExecutor brokerChannelExecutor) { // synchronous return new ExecutorSubscribableChannel(null); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/DelegatingWebSocketConfiguration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/DelegatingWebSocketConfiguration.java index 433f8bb2f363..561a09801576 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/DelegatingWebSocketConfiguration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/DelegatingWebSocketConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -29,9 +29,10 @@ * configure WebSocket request handling. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 4.0 */ -@Configuration +@Configuration(proxyBeanMethods = false) public class DelegatingWebSocketConfiguration extends WebSocketConfigurationSupport { private final List configurers = new ArrayList<>(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/DelegatingWebSocketMessageBrokerConfiguration.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/DelegatingWebSocketMessageBrokerConfiguration.java index d6610043fc43..1e0d704ce8a5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/DelegatingWebSocketMessageBrokerConfiguration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/DelegatingWebSocketMessageBrokerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -37,9 +37,10 @@ *

    This class is typically imported via {@link EnableWebSocketMessageBroker}. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 4.0 */ -@Configuration +@Configuration(proxyBeanMethods = false) public class DelegatingWebSocketMessageBrokerConfiguration extends WebSocketMessageBrokerConfigurationSupport { private final List configurers = new ArrayList<>(); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketConfigurationSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketConfigurationSupport.java index 410beb1732f4..7a2cb8981b78 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketConfigurationSupport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketConfigurationSupport.java @@ -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. @@ -27,6 +27,7 @@ * Configuration support for WebSocket request handling. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 4.0 */ public class WebSocketConfigurationSupport { @@ -39,10 +40,10 @@ public class WebSocketConfigurationSupport { @Bean - public HandlerMapping webSocketHandlerMapping() { + public HandlerMapping webSocketHandlerMapping(@Nullable TaskScheduler defaultSockJsTaskScheduler) { ServletWebSocketHandlerRegistry registry = initHandlerRegistry(); if (registry.requiresTaskScheduler()) { - TaskScheduler scheduler = defaultSockJsTaskScheduler(); + TaskScheduler scheduler = defaultSockJsTaskScheduler; Assert.notNull(scheduler, "Expected default TaskScheduler bean"); registry.setTaskScheduler(scheduler); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java index 477baf6c2528..9fbb37763331 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java @@ -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. @@ -19,15 +19,19 @@ import org.springframework.beans.factory.config.CustomScopeConfigurer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.core.task.TaskExecutor; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.lang.Nullable; import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpSessionScope; import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler; import org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration; import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler; import org.springframework.messaging.simp.user.SimpUserRegistry; +import org.springframework.messaging.support.AbstractSubscribableChannel; +import org.springframework.scheduling.TaskScheduler; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.config.WebSocketMessageBrokerStats; @@ -46,6 +50,7 @@ * * @author Rossen Stoyanchev * @author Artem Bilan + * @author Sebastien Deleuze * @since 4.0 */ public abstract class WebSocketMessageBrokerConfigurationSupport extends AbstractMessageBrokerConfiguration { @@ -55,9 +60,10 @@ public abstract class WebSocketMessageBrokerConfigurationSupport extends Abstrac @Override - protected SimpAnnotationMethodMessageHandler createAnnotationMethodMessageHandler() { - return new WebSocketAnnotationMethodMessageHandler( - clientInboundChannel(), clientOutboundChannel(), brokerMessagingTemplate()); + protected SimpAnnotationMethodMessageHandler createAnnotationMethodMessageHandler( + AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel, + SimpMessagingTemplate brokerMessagingTemplate) { + return new WebSocketAnnotationMethodMessageHandler(clientInboundChannel, clientOutboundChannel, brokerMessagingTemplate); } @Override @@ -70,10 +76,11 @@ protected SimpUserRegistry createLocalUserRegistry(@Nullable Integer order) { } @Bean - public HandlerMapping stompWebSocketHandlerMapping() { - WebSocketHandler handler = decorateWebSocketHandler(subProtocolWebSocketHandler()); + public HandlerMapping stompWebSocketHandlerMapping(WebSocketHandler subProtocolWebSocketHandler, + TaskScheduler messageBrokerTaskScheduler) { + WebSocketHandler handler = decorateWebSocketHandler(subProtocolWebSocketHandler); WebMvcStompEndpointRegistry registry = new WebMvcStompEndpointRegistry( - handler, getTransportRegistration(), messageBrokerTaskScheduler()); + handler, getTransportRegistration(), messageBrokerTaskScheduler); ApplicationContext applicationContext = getApplicationContext(); if (applicationContext != null) { registry.setApplicationContext(applicationContext); @@ -83,8 +90,9 @@ public HandlerMapping stompWebSocketHandlerMapping() { } @Bean - public WebSocketHandler subProtocolWebSocketHandler() { - return new SubProtocolWebSocketHandler(clientInboundChannel(), clientOutboundChannel()); + public WebSocketHandler subProtocolWebSocketHandler(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel) { + return new SubProtocolWebSocketHandler(clientInboundChannel, clientOutboundChannel); } protected WebSocketHandler decorateWebSocketHandler(WebSocketHandler handler) { @@ -115,20 +123,17 @@ public static CustomScopeConfigurer webSocketScopeConfigurer() { } @Bean - public WebSocketMessageBrokerStats webSocketMessageBrokerStats() { - AbstractBrokerMessageHandler relayBean = stompBrokerRelayMessageHandler(); - - // Ensure STOMP endpoints are registered - stompWebSocketHandlerMapping(); - + public WebSocketMessageBrokerStats webSocketMessageBrokerStats(@Nullable AbstractBrokerMessageHandler stompBrokerRelayMessageHandler, + WebSocketHandler subProtocolWebSocketHandler, TaskExecutor clientInboundChannelExecutor, TaskExecutor clientOutboundChannelExecutor, + TaskScheduler messageBrokerTaskScheduler) { WebSocketMessageBrokerStats stats = new WebSocketMessageBrokerStats(); - stats.setSubProtocolWebSocketHandler((SubProtocolWebSocketHandler) subProtocolWebSocketHandler()); - if (relayBean instanceof StompBrokerRelayMessageHandler) { - stats.setStompBrokerRelay((StompBrokerRelayMessageHandler) relayBean); + stats.setSubProtocolWebSocketHandler((SubProtocolWebSocketHandler) subProtocolWebSocketHandler); + if (stompBrokerRelayMessageHandler instanceof StompBrokerRelayMessageHandler) { + stats.setStompBrokerRelay((StompBrokerRelayMessageHandler) stompBrokerRelayMessageHandler); } - stats.setInboundChannelExecutor(clientInboundChannelExecutor()); - stats.setOutboundChannelExecutor(clientOutboundChannelExecutor()); - stats.setSockJsTaskScheduler(messageBrokerTaskScheduler()); + stats.setInboundChannelExecutor(clientInboundChannelExecutor); + stats.setOutboundChannelExecutor(clientOutboundChannelExecutor); + stats.setSockJsTaskScheduler(messageBrokerTaskScheduler); return stats; } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java index 63fed01115b5..e4ea206c0ebc 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupportTests.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -67,6 +68,7 @@ * Test fixture for {@link WebSocketMessageBrokerConfigurationSupport}. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class WebSocketMessageBrokerConfigurationSupportTests { @@ -251,24 +253,25 @@ static class TestChannelConfig extends DelegatingWebSocketMessageBrokerConfigura @Override @Bean - public AbstractSubscribableChannel clientInboundChannel() { + public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInboundChannelExecutor) { TestChannel channel = new TestChannel(); - channel.setInterceptors(super.clientInboundChannel().getInterceptors()); + channel.setInterceptors(super.clientInboundChannel(clientInboundChannelExecutor).getInterceptors()); return channel; } @Override @Bean - public AbstractSubscribableChannel clientOutboundChannel() { + public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutboundChannelExecutor) { TestChannel channel = new TestChannel(); - channel.setInterceptors(super.clientOutboundChannel().getInterceptors()); + channel.setInterceptors(super.clientOutboundChannel(clientOutboundChannelExecutor).getInterceptors()); return channel; } @Override - public AbstractSubscribableChannel brokerChannel() { + public AbstractSubscribableChannel brokerChannel(AbstractSubscribableChannel clientInboundChannel, + AbstractSubscribableChannel clientOutboundChannel, TaskExecutor brokerChannelExecutor) { TestChannel channel = new TestChannel(); - channel.setInterceptors(super.brokerChannel().getInterceptors()); + channel.setInterceptors(super.brokerChannel(clientInboundChannel, clientOutboundChannel, brokerChannelExecutor).getInterceptors()); return channel; } } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompWebSocketIntegrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompWebSocketIntegrationTests.java index ea48d984ee87..435a5552a9c1 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompWebSocketIntegrationTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompWebSocketIntegrationTests.java @@ -31,6 +31,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.core.task.TaskExecutor; import org.springframework.messaging.handler.annotation.MessageExceptionHandler; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.annotation.SendToUser; @@ -59,6 +60,7 @@ * * @author Rossen Stoyanchev * @author Sam Brannen + * @author Sebastien Deleuze */ class StompWebSocketIntegrationTests extends AbstractWebSocketIntegrationTests { @@ -331,13 +333,13 @@ static class TestMessageBrokerConfiguration extends DelegatingWebSocketMessageBr @Override @Bean - public AbstractSubscribableChannel clientInboundChannel() { + public AbstractSubscribableChannel clientInboundChannel(TaskExecutor clientInboundChannelExecutor) { return new ExecutorSubscribableChannel(); // synchronous } @Override @Bean - public AbstractSubscribableChannel clientOutboundChannel() { + public AbstractSubscribableChannel clientOutboundChannel(TaskExecutor clientOutboundChannelExecutor) { return new ExecutorSubscribableChannel(); // synchronous } } From 7ef3257b031000b9f092d4014902bd676c607c9c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Dec 2020 17:44:22 +0000 Subject: [PATCH 0108/1294] Correctly determine HttpServletMapping for INCLUDE Closes gh-26216 --- .../java/org/springframework/web/util/UrlPathHelper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index c760ea44c56b..f283af87feb9 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Properties; +import javax.servlet.RequestDispatcher; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletMapping; import javax.servlet.http.HttpServletRequest; @@ -774,7 +775,10 @@ public String removeSemicolonContent(String requestUri) { private static class Servlet4Delegate { public static boolean skipServletPathDetermination(HttpServletRequest request) { - HttpServletMapping mapping = request.getHttpServletMapping(); + HttpServletMapping mapping = (HttpServletMapping) request.getAttribute(RequestDispatcher.INCLUDE_MAPPING); + if (mapping == null) { + mapping = request.getHttpServletMapping(); + } MappingMatch match = mapping.getMappingMatch(); return (match != null && (!match.equals(MappingMatch.PATH) || mapping.getPattern().equals("/*"))); } From 5efa4ad4429f078c58789da8bdcd07ca139da4aa Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 7 Dec 2020 20:38:07 +0100 Subject: [PATCH 0109/1294] Pull images with registry-image resource in build --- ci/pipeline.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 38e7a4f4d8be..7e9270ff48df 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -37,17 +37,17 @@ anchors: resource_types: - name: artifactory-resource - type: docker-image + type: registry-image source: repository: springio/artifactory-resource tag: 0.0.12 - name: github-status-resource - type: docker-image + type: registry-image source: repository: dpb587/github-status-resource tag: master - name: slack-notification - type: docker-image + type: registry-image source: repository: cfcommunity/slack-notification-resource tag: latest From 834032df1f22de7251c5dfbfa77bad443ee176f8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Dec 2020 22:06:22 +0100 Subject: [PATCH 0110/1294] Clarify intended advice execution behavior (includes related polishing) Closes gh-26202 --- src/docs/asciidoc/core/core-aop.adoc | 203 +++++++++++++-------------- 1 file changed, 96 insertions(+), 107 deletions(-) diff --git a/src/docs/asciidoc/core/core-aop.adoc b/src/docs/asciidoc/core/core-aop.adoc index ffc3be35770a..93bc96c25564 100644 --- a/src/docs/asciidoc/core/core-aop.adoc +++ b/src/docs/asciidoc/core/core-aop.adoc @@ -925,7 +925,6 @@ You can declare before advice in an aspect by using the `@Before` annotation: public void doAccessCheck() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -941,7 +940,6 @@ You can declare before advice in an aspect by using the `@Before` annotation: fun doAccessCheck() { // ... } - } ---- @@ -961,7 +959,6 @@ following example: public void doAccessCheck() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim",role="secondary"] @@ -977,7 +974,6 @@ following example: fun doAccessCheck() { // ... } - } ---- @@ -985,8 +981,8 @@ following example: [[aop-advice-after-returning]] ==== After Returning Advice -After returning advice runs when a matched method execution returns normally. You can -declare it by using the `@AfterReturning` annotation: +After returning advice runs when a matched method execution returns normally. +You can declare it by using the `@AfterReturning` annotation: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1001,7 +997,6 @@ declare it by using the `@AfterReturning` annotation: public void doAccessCheck() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1017,16 +1012,16 @@ declare it by using the `@AfterReturning` annotation: fun doAccessCheck() { // ... } - + } ---- -NOTE: You can have multiple advice declarations (and other members -as well), all inside the same aspect. We show only a single advice declaration in -these examples to focus the effect of each one. +NOTE: You can have multiple advice declarations (and other members as well), +all inside the same aspect. We show only a single advice declaration in these +examples to focus the effect of each one. -Sometimes, you need access in the advice body to the actual value that was returned. You -can use the form of `@AfterReturning` that binds the return value to get that access, as -the following example shows: +Sometimes, you need access in the advice body to the actual value that was returned. +You can use the form of `@AfterReturning` that binds the return value to get that +access, as the following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1043,7 +1038,6 @@ the following example shows: public void doAccessCheck(Object retVal) { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1061,15 +1055,14 @@ the following example shows: fun doAccessCheck(retVal: Any) { // ... } - } ---- -The name used in the `returning` attribute must correspond to the name of a parameter in -the advice method. When a method execution returns, the return value is passed to +The name used in the `returning` attribute must correspond to the name of a parameter +in the advice method. When a method execution returns, the return value is passed to the advice method as the corresponding argument value. A `returning` clause also -restricts matching to only those method executions that return a value of the specified -type (in this case, `Object`, which matches any return value). +restricts matching to only those method executions that return a value of the +specified type (in this case, `Object`, which matches any return value). Please note that it is not possible to return a totally different reference when using after returning advice. @@ -1079,8 +1072,8 @@ using after returning advice. ==== After Throwing Advice After throwing advice runs when a matched method execution exits by throwing an -exception. You can declare it by using the `@AfterThrowing` annotation, as the following -example shows: +exception. You can declare it by using the `@AfterThrowing` annotation, as the +following example shows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1095,7 +1088,6 @@ example shows: public void doRecoveryActions() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1111,15 +1103,14 @@ example shows: fun doRecoveryActions() { // ... } - } ---- -Often, you want the advice to run only when exceptions of a given type are thrown, and -you also often need access to the thrown exception in the advice body. You can use the -`throwing` attribute to both restrict matching (if desired -- use `Throwable` as the -exception type otherwise) and bind the thrown exception to an advice parameter. The -following example shows how to do so: +Often, you want the advice to run only when exceptions of a given type are thrown, +and you also often need access to the thrown exception in the advice body. You can +use the `throwing` attribute to both restrict matching (if desired -- use `Throwable` +as the exception type otherwise) and bind the thrown exception to an advice parameter. +The following example shows how to do so: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1136,7 +1127,6 @@ following example shows how to do so: public void doRecoveryActions(DataAccessException ex) { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1154,15 +1144,22 @@ following example shows how to do so: fun doRecoveryActions(ex: DataAccessException) { // ... } - } ---- The name used in the `throwing` attribute must correspond to the name of a parameter in the advice method. When a method execution exits by throwing an exception, the exception -is passed to the advice method as the corresponding argument value. A `throwing` -clause also restricts matching to only those method executions that throw an exception -of the specified type ( `DataAccessException`, in this case). +is passed to the advice method as the corresponding argument value. A `throwing` clause +also restricts matching to only those method executions that throw an exception of the +specified type (`DataAccessException`, in this case). + +[NOTE] +==== +Note that `@AfterThrowing` does not indicate a general exception handling callback. +Specifically, an `@AfterThrowing` advice method is only supposed to receive exceptions +from the join point (user-declared target method) itself but not from an accompanying +`@After`/`@AfterReturning` method. +==== [[aop-advice-after-finally]] @@ -1170,8 +1167,8 @@ of the specified type ( `DataAccessException`, in this case). After (finally) advice runs when a matched method execution exits. It is declared by using the `@After` annotation. After advice must be prepared to handle both normal and -exception return conditions. It is typically used for releasing resources and similar purposes. -The following example shows how to use after finally advice: +exception return conditions. It is typically used for releasing resources and similar +purposes. The following example shows how to use after finally advice: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1186,7 +1183,6 @@ The following example shows how to use after finally advice: public void doReleaseLock() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1202,30 +1198,37 @@ The following example shows how to use after finally advice: fun doReleaseLock() { // ... } - } ---- +[NOTE] +==== +Note that `@After` advice in AspectJ is defined as "after finally advice", analogous +to a finally block in a try-catch statement. It will be invoked for any outcome, +normal return or exception thrown from the join point (user-declared target method), +in contrast to `@AfterReturning` which only applies to successful normal returns. +==== + [[aop-ataspectj-around-advice]] ==== Around Advice -The last kind of advice is around advice. Around advice runs "`around`" a matched method's -execution. It has the opportunity to do work both before and after the method runs -and to determine when, how, and even if the method actually gets to run at all. +The last kind of advice is around advice. Around advice runs "`around`" a matched +method's execution. It has the opportunity to do work both before and after the method +runs and to determine when, how, and even if the method actually gets to run at all. Around advice is often used if you need to share state before and after a method -execution in a thread-safe manner (starting and stopping a timer, for example). Always -use the least powerful form of advice that meets your requirements (that is, do not use -around advice if before advice would do). +execution in a thread-safe manner (starting and stopping a timer, for example). +Always use the least powerful form of advice that meets your requirements (that is, +do not use around advice if before advice would do). Around advice is declared by using the `@Around` annotation. The first parameter of the advice method must be of type `ProceedingJoinPoint`. Within the body of the advice, -calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to -run. The `proceed` method can also pass in an `Object[]`. The values -in the array are used as the arguments to the method execution when it proceeds. +calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to run. +The `proceed` method can also pass in an `Object[]`. The values in the array are used +as the arguments to the method execution when it proceeds. -NOTE: The behavior of `proceed` when called with an `Object[]` is a little different than the -behavior of `proceed` for around advice compiled by the AspectJ compiler. For around +NOTE: The behavior of `proceed` when called with an `Object[]` is a little different than +the behavior of `proceed` for around advice compiled by the AspectJ compiler. For around advice written using the traditional AspectJ language, the number of arguments passed to `proceed` must match the number of arguments passed to the around advice (not the number of arguments taken by the underlying join point), and the value passed to proceed in a @@ -1257,7 +1260,6 @@ The following example shows how to use around advice: // stop stopwatch return retVal; } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1277,34 +1279,31 @@ The following example shows how to use around advice: // stop stopwatch return retVal } - } ---- -The value returned by the around advice is the return value seen by the caller of -the method. For example, a simple caching aspect could return a value from a cache if it +The value returned by the around advice is the return value seen by the caller of the +method. For example, a simple caching aspect could return a value from a cache if it has one and invoke `proceed()` if it does not. Note that `proceed` may be invoked once, -many times, or not at all within the body of the around advice. All of these are -legal. +many times, or not at all within the body of the around advice. All of these are legal. [[aop-ataspectj-advice-params]] ==== Advice Parameters -Spring offers fully typed advice, meaning that you declare the parameters you need -in the advice signature (as we saw earlier for the returning and throwing examples) rather -than work with `Object[]` arrays all the time. We see how to make argument and other -contextual values available to the advice body later in this section. First, we take a look at -how to write generic advice that can find out about the method the advice is currently -advising. +Spring offers fully typed advice, meaning that you declare the parameters you need in the +advice signature (as we saw earlier for the returning and throwing examples) rather than +work with `Object[]` arrays all the time. We see how to make argument and other contextual +values available to the advice body later in this section. First, we take a look at how to +write generic advice that can find out about the method the advice is currently advising. [[aop-ataspectj-advice-params-the-joinpoint]] ===== Access to the Current `JoinPoint` Any advice method may declare, as its first parameter, a parameter of type -`org.aspectj.lang.JoinPoint` (note that around advice is required to declare -a first parameter of type `ProceedingJoinPoint`, which is a subclass of `JoinPoint`. The -`JoinPoint` interface provides a number of useful methods: +`org.aspectj.lang.JoinPoint` (note that around advice is required to declare a first +parameter of type `ProceedingJoinPoint`, which is a subclass of `JoinPoint`. +The `JoinPoint` interface provides a number of useful methods: * `getArgs()`: Returns the method arguments. * `getThis()`: Returns the proxy object. @@ -1320,9 +1319,9 @@ See the https://www.eclipse.org/aspectj/doc/released/runtime-api/org/aspectj/lan We have already seen how to bind the returned value or exception value (using after returning and after throwing advice). To make argument values available to the advice body, you can use the binding form of `args`. If you use a parameter name in place of a -type name in an args expression, the value of the corresponding argument is -passed as the parameter value when the advice is invoked. An example should make this -clearer. Suppose you want to advise the execution of DAO operations that take an `Account` +type name in an args expression, the value of the corresponding argument is passed as +the parameter value when the advice is invoked. An example should make this clearer. +Suppose you want to advise the execution of DAO operations that take an `Account` object as the first parameter, and you need access to the account in the advice body. You could write the following: @@ -1654,20 +1653,23 @@ the higher precedence. [NOTE] ==== +Each of the distinct advice types of a particular aspect is conceptually meant to apply +to the join point directly. As a consequence, an `@AfterThrowing` advice method is not +supposed to receive an exception from an accompanying `@After`/`@AfterReturning` method. + As of Spring Framework 5.2.7, advice methods defined in the same `@Aspect` class that need to run at the same join point are assigned precedence based on their advice type in the following order, from highest to lowest precedence: `@Around`, `@Before`, `@After`, -`@AfterReturning`, `@AfterThrowing`. Note, however, that due to the implementation style -in Spring's `AspectJAfterAdvice`, an `@After` advice method will effectively be invoked -after any `@AfterReturning` or `@AfterThrowing` advice methods in the same aspect. +`@AfterReturning`, `@AfterThrowing`. Note, however, that an `@After` advice method will +effectively be invoked after any `@AfterReturning` or `@AfterThrowing` advice methods +in the same aspect, following AspectJ's "after finally advice" semantics for `@After`. When two pieces of the same type of advice (for example, two `@After` advice methods) defined in the same `@Aspect` class both need to run at the same join point, the ordering is undefined (since there is no way to retrieve the source code declaration order through reflection for javac-compiled classes). Consider collapsing such advice methods into one -advice method per join point in each `@Aspect` class or refactor the pieces of advice -into separate `@Aspect` classes that you can order at the aspect level via `Ordered` or -`@Order`. +advice method per join point in each `@Aspect` class or refactor the pieces of advice into +separate `@Aspect` classes that you can order at the aspect level via `Ordered` or `@Order`. ==== @@ -1678,11 +1680,11 @@ Introductions (known as inter-type declarations in AspectJ) enable an aspect to that advised objects implement a given interface, and to provide an implementation of that interface on behalf of those objects. -You can make an introduction by using the `@DeclareParents` annotation. This annotation is used -to declare that matching types have a new parent (hence the name). For example, given an -interface named `UsageTracked` and an implementation of that interface named `DefaultUsageTracked`, -the following aspect declares that all implementors of service interfaces also implement -the `UsageTracked` interface (to expose statistics via JMX for example): +You can make an introduction by using the `@DeclareParents` annotation. This annotation +is used to declare that matching types have a new parent (hence the name). For example, +given an interface named `UsageTracked` and an implementation of that interface named +`DefaultUsageTracked`, the following aspect declares that all implementors of service +interfaces also implement the `UsageTracked` interface (e.g. for statistics via JMX): [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -1764,7 +1766,6 @@ annotation. Consider the following example: public void recordServiceUsage() { // ... } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -1779,7 +1780,6 @@ annotation. Consider the following example: fun recordServiceUsage() { // ... } - } ---- @@ -1854,7 +1854,6 @@ call `proceed` multiple times. The following listing shows the basic aspect impl } while(numAttempts <= this.maxRetries); throw lockFailureException; } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] @@ -2066,7 +2065,6 @@ as the following example shows: expression="execution(* com.xyz.myapp.service.*.*(..))"/> ... - @@ -2088,7 +2086,6 @@ collects the `this` object as the join point context and passes it to the advice ... - @@ -2179,7 +2176,6 @@ a `pointcut` attribute, as follows: method="doAccessCheck"/> ... - ---- @@ -2209,7 +2205,6 @@ shows how to declare it: method="doAccessCheck"/> ... - ---- @@ -2227,7 +2222,6 @@ the return value should be passed, as the following example shows: method="doAccessCheck"/> ... - ---- @@ -2263,7 +2257,6 @@ as the following example shows: method="doRecoveryActions"/> ... - ---- @@ -2281,13 +2274,12 @@ which the exception should be passed as the following example shows: method="doRecoveryActions"/> ... - ---- -The `doRecoveryActions` method must declare a parameter named `dataAccessEx`. The type of -this parameter constrains matching in the same way as described for `@AfterThrowing`. For -example, the method signature may be declared as follows: +The `doRecoveryActions` method must declare a parameter named `dataAccessEx`. +The type of this parameter constrains matching in the same way as described for +`@AfterThrowing`. For example, the method signature may be declared as follows: [source,java,indent=0,subs="verbatim,quotes",role="primary"] .Java @@ -2304,8 +2296,8 @@ example, the method signature may be declared as follows: [[aop-schema-advice-after-finally]] ==== After (Finally) Advice -After (finally) advice runs no matter how a matched method execution exits. You can declare it -by using the `after` element, as the following example shows: +After (finally) advice runs no matter how a matched method execution exits. +You can declare it by using the `after` element, as the following example shows: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -2316,7 +2308,6 @@ by using the `after` element, as the following example shows: method="doReleaseLock"/> ... - ---- @@ -2327,17 +2318,17 @@ by using the `after` element, as the following example shows: The last kind of advice is around advice. Around advice runs "around" a matched method execution. It has the opportunity to do work both before and after the method runs and to determine when, how, and even if the method actually gets to run at all. -Around advice is often used to share state before and after a method -execution in a thread-safe manner (starting and stopping a timer, for example). Always -use the least powerful form of advice that meets your requirements. Do not use around -advice if before advice can do the job. - -You can declare around advice by using the `aop:around` element. The first parameter of the -advice method must be of type `ProceedingJoinPoint`. Within the body of the advice, -calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to -run. The `proceed` method may also be called with an `Object[]`. The values -in the array are used as the arguments to the method execution when it proceeds. See -<> for notes on calling `proceed` with an `Object[]`. +Around advice is often used to share state before and after a method execution in a +thread-safe manner (starting and stopping a timer, for example). Always use the least +powerful form of advice that meets your requirements. Do not use around advice if +before advice can do the job. + +You can declare around advice by using the `aop:around` element. The first parameter of +the advice method must be of type `ProceedingJoinPoint`. Within the body of the advice, +calling `proceed()` on the `ProceedingJoinPoint` causes the underlying method to run. +The `proceed` method may also be called with an `Object[]`. The values in the array +are used as the arguments to the method execution when it proceeds. +See <> for notes on calling `proceed` with an `Object[]`. The following example shows how to declare around advice in XML: [source,xml,indent=0,subs="verbatim,quotes"] @@ -2349,7 +2340,6 @@ The following example shows how to declare around advice in XML: method="doBasicProfiling"/> ... - ---- @@ -2763,7 +2753,6 @@ call `proceed` multiple times. The following listing shows the basic aspect impl } while(numAttempts <= this.maxRetries); throw lockFailureException; } - } ---- [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] From c970c318f4ecf5f23e3f17aa3a22979a483f9dab Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Dec 2020 22:08:01 +0100 Subject: [PATCH 0111/1294] Polishing --- .../autoproxy/AtAspectJAfterThrowingTests.java | 14 ++++++++------ .../autoproxy/AtAspectJAnnotationBindingTests.java | 6 +++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java index 13bd4d38c0e9..1c6dd13a16ae 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAfterThrowingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -36,24 +36,26 @@ public class AtAspectJAfterThrowingTests { @Test - public void testAccessThrowable() throws Exception { + public void testAccessThrowable() { ClassPathXmlApplicationContext ctx = - new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); + new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-context.xml", getClass()); ITestBean bean = (ITestBean) ctx.getBean("testBean"); ExceptionHandlingAspect aspect = (ExceptionHandlingAspect) ctx.getBean("aspect"); assertThat(AopUtils.isAopProxy(bean)).isTrue(); + IOException exceptionThrown = null; try { bean.unreliableFileOperation(); } - catch (IOException e) { - // + catch (IOException ex) { + exceptionThrown = ex; } assertThat(aspect.handled).isEqualTo(1); - assertThat(aspect.lastException).isNotNull(); + assertThat(aspect.lastException).isSameAs(exceptionThrown); } + } diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java index f6fdf729ab6a..679a4ceff945 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AtAspectJAnnotationBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -36,6 +36,7 @@ public class AtAspectJAnnotationBindingTests { private AnnotatedTestBean testBean; + private ClassPathXmlApplicationContext ctx; @@ -70,8 +71,7 @@ public void testPointcutEvaluatedAgainstArray() { class AtAspectJAnnotationBindingTestAspect { @Around("execution(* *(..)) && @annotation(testAnn)") - public Object doWithAnnotation(ProceedingJoinPoint pjp, TestAnnotation testAnn) - throws Throwable { + public Object doWithAnnotation(ProceedingJoinPoint pjp, TestAnnotation testAnn) throws Throwable { String annValue = testAnn.value(); Object result = pjp.proceed(); return (result instanceof String ? annValue + " " + result : result); From 10f6a223152a561ff23f8af289f0d830f1536036 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 7 Dec 2020 22:08:50 +0100 Subject: [PATCH 0112/1294] Upgrade to Undertow 2.2.3 and Apache HttpCore Reactive 5.0.3 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ac605cca33f5..549cbe26fdfe 100644 --- a/build.gradle +++ b/build.gradle @@ -139,7 +139,7 @@ configure(allprojects) { project -> entry 'tomcat-embed-core' entry 'tomcat-embed-websocket' } - dependencySet(group: 'io.undertow', version: '2.2.2.Final') { + dependencySet(group: 'io.undertow', version: '2.2.3.Final') { entry 'undertow-core' entry('undertow-websockets-jsr') { exclude group: "org.jboss.spec.javax.websocket", name: "jboss-websocket-api_1.1_spec" @@ -161,7 +161,7 @@ configure(allprojects) { project -> exclude group: "commons-logging", name: "commons-logging" } dependency 'org.apache.httpcomponents.client5:httpclient5:5.0.3' - dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.2' + dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.3' dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.4" dependency "org.jruby:jruby:9.2.13.0" From ad420107852bafa141dcd68120be6d6a24153bf8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Dec 2020 20:02:44 +0000 Subject: [PATCH 0113/1294] Correlate data buffers to request log messages HttpMessageWriter implementations now attach the request log prefix as a hint to created data buffers when the logger associated with the writer is at DEBUG level. Closes gh-26230 --- .../org/springframework/core/codec/Hints.java | 20 +++++++++++++++++++ .../core/codec/ResourceRegionEncoder.java | 5 ++++- .../core/io/buffer/DataBufferUtils.java | 18 +++++++++++++++++ .../core/io/buffer/NettyDataBuffer.java | 6 ++++++ .../core/io/buffer/PooledDataBuffer.java | 9 ++++++++- .../io/buffer/LeakAwareDataBuffer.java | 19 +++++++++--------- .../http/codec/EncoderHttpMessageWriter.java | 16 +++++++++++++-- .../http/codec/ResourceHttpMessageWriter.java | 5 ++++- .../ServerSentEventHttpMessageWriter.java | 8 +++++++- .../multipart/MultipartHttpMessageWriter.java | 4 ++++ .../multipart/PartHttpMessageWriter.java | 4 ++++ 11 files changed, 99 insertions(+), 15 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/codec/Hints.java b/spring-core/src/main/java/org/springframework/core/codec/Hints.java index 7f167fd509ee..a731b018bd67 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/Hints.java +++ b/spring-core/src/main/java/org/springframework/core/codec/Hints.java @@ -21,6 +21,8 @@ import org.apache.commons.logging.Log; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; @@ -148,4 +150,22 @@ public static Map merge(Map hints, String hintNa } } + /** + * If the hints contain a {@link #LOG_PREFIX_HINT} and the given logger has + * DEBUG level enabled, apply the log prefix as a hint to the given buffer + * via {@link DataBufferUtils#touch(DataBuffer, Object)}. + * @param buffer the buffer to touch + * @param hints the hints map to check for a log prefix + * @param logger the logger whose level to check + * @since 5.3.2 + */ + public static void touchDataBuffer(DataBuffer buffer, @Nullable Map hints, Log logger) { + if (logger.isDebugEnabled() && hints != null) { + Object logPrefix = hints.get(LOG_PREFIX_HINT); + if (logPrefix != null) { + DataBufferUtils.touch(buffer, logPrefix); + } + } + } + } diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java index 3e9d19c20d75..3330ac6e1555 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -130,6 +130,9 @@ private Flux writeResourceRegion( } Flux in = DataBufferUtils.read(resource, position, bufferFactory, this.bufferSize); + if (logger.isDebugEnabled()) { + in = in.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger)); + } return DataBufferUtils.takeUntilByteCount(in, count); } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index 81d0b018449b..41a3760fbe32 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -487,6 +487,24 @@ public static T retain(T dataBuffer) { } } + /** + * Associate the given hint with the data buffer if it is a pooled buffer + * and supports leak tracking. + * @param dataBuffer the data buffer to attach the hint to + * @param hint the hint to attach + * @return the input buffer + * @since 5.3.2 + */ + @SuppressWarnings("unchecked") + public static T touch(T dataBuffer, Object hint) { + if (dataBuffer instanceof PooledDataBuffer) { + return (T) ((PooledDataBuffer) dataBuffer).touch(hint); + } + else { + return dataBuffer; + } + } + /** * Release the given data buffer, if it is a {@link PooledDataBuffer} and * has been {@linkplain PooledDataBuffer#isAllocated() allocated}. diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java index 7b37f8bd51e5..7809c652959d 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/NettyDataBuffer.java @@ -315,6 +315,12 @@ public PooledDataBuffer retain() { return new NettyDataBuffer(this.byteBuf.retain(), this.dataBufferFactory); } + @Override + public PooledDataBuffer touch(Object hint) { + this.byteBuf.touch(hint); + return this; + } + @Override public boolean release() { return this.byteBuf.release(); diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java index d97e299b0f66..e3e794214e17 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/PooledDataBuffer.java @@ -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. @@ -38,6 +38,13 @@ public interface PooledDataBuffer extends DataBuffer { */ PooledDataBuffer retain(); + /** + * Associate the given hint with the data buffer for debugging purposes. + * @return this buffer + * @since 5.3.2 + */ + PooledDataBuffer touch(Object hint); + /** * Decrease the reference count for this buffer by one, * and deallocate it once the count reaches zero. diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBuffer.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBuffer.java index f03036f13109..32fe8c5e0aca 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBuffer.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/LeakAwareDataBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -17,6 +17,7 @@ package org.springframework.core.testfixture.io.buffer; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DataBufferWrapper; import org.springframework.core.io.buffer.PooledDataBuffer; import org.springframework.util.Assert; @@ -67,19 +68,19 @@ public boolean isAllocated() { @Override public PooledDataBuffer retain() { - DataBuffer delegate = dataBuffer(); - if (delegate instanceof PooledDataBuffer) { - ((PooledDataBuffer) delegate).retain(); - } + DataBufferUtils.retain(dataBuffer()); + return this; + } + + @Override + public PooledDataBuffer touch(Object hint) { + DataBufferUtils.touch(dataBuffer(), hint); return this; } @Override public boolean release() { - DataBuffer delegate = dataBuffer(); - if (delegate instanceof PooledDataBuffer) { - ((PooledDataBuffer) delegate).release(); - } + DataBufferUtils.release(dataBuffer()); return isAllocated(); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java index 5a63145b4be9..c429767fbfb7 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java @@ -57,6 +57,9 @@ */ public class EncoderHttpMessageWriter implements HttpMessageWriter { + private static final Log logger = HttpLogging.forLogName(EncoderHttpMessageWriter.class); + + private final Encoder encoder; private final List mediaTypes; @@ -125,6 +128,7 @@ public Mono write(Publisher inputStream, ResolvableType eleme return message.setComplete().then(Mono.empty()); })) .flatMap(buffer -> { + Hints.touchDataBuffer(buffer, hints, logger); message.getHeaders().setContentLength(buffer.readableByteCount()); return message.writeWith(Mono.just(buffer) .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)); @@ -132,10 +136,15 @@ public Mono write(Publisher inputStream, ResolvableType eleme } if (isStreamingMediaType(contentType)) { - return message.writeAndFlushWith(body.map(buffer -> - Mono.just(buffer).doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release))); + return message.writeAndFlushWith(body.map(buffer -> { + Hints.touchDataBuffer(buffer, hints, logger); + return Mono.just(buffer).doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + })); } + if (logger.isDebugEnabled()) { + body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger)); + } return message.writeWith(body); } @@ -166,6 +175,9 @@ private static MediaType addDefaultCharset(MediaType main, @Nullable MediaType d return main; } + private static void touch(DataBuffer buffer, Map hints) { + } + private boolean isStreamingMediaType(@Nullable MediaType mediaType) { if (mediaType == null || !(this.encoder instanceof HttpMessageEncoder)) { return false; diff --git a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java index 4376d39d06c6..af456989e01b 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ResourceHttpMessageWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -132,6 +132,9 @@ private Mono writeResource(Resource resource, ResolvableType type, @Nullab Mono input = Mono.just(resource); DataBufferFactory factory = message.bufferFactory(); Flux body = this.encoder.encode(input, factory, type, resourceMediaType, hints); + if (logger.isDebugEnabled()) { + body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger)); + } return message.writeWith(body); }); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java index 90e4f74ba978..6f54d9631b18 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageWriter.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; +import org.apache.commons.logging.Log; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -35,6 +36,7 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.PooledDataBuffer; +import org.springframework.http.HttpLogging; import org.springframework.http.MediaType; import org.springframework.http.ReactiveHttpOutputMessage; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -57,6 +59,8 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter WRITABLE_MEDIA_TYPES = Collections.singletonList(MediaType.TEXT_EVENT_STREAM); + private static final Log logger = HttpLogging.forLogName(ServerSentEventHttpMessageWriter.class); + @Nullable private final Encoder encoder; @@ -167,9 +171,11 @@ private Flux encodeEvent(StringBuilder eventContent, T data, Res if (this.encoder == null) { throw new CodecException("No SSE encoder configured and the data is not String."); } + DataBuffer buffer = ((Encoder) this.encoder).encodeValue(data, factory, dataType, mediaType, hints); + Hints.touchDataBuffer(buffer, hints, logger); return Flux.just(factory.join(Arrays.asList( encodeText(eventContent, mediaType, factory), - ((Encoder) this.encoder).encodeValue(data, factory, dataType, mediaType, hints), + buffer, encodeText("\n\n", mediaType, factory)))); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java index d94104535d11..f2f490e1d888 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java @@ -199,6 +199,10 @@ private Mono writeMultipart(MultiValueMap map, .concatWith(generateLastLine(boundary, bufferFactory)) .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + if (logger.isDebugEnabled()) { + body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger)); + } + return outputMessage.writeWith(body); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java index aa7819b2d8b5..31817470ca3d 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java @@ -69,6 +69,10 @@ public Mono write(Publisher parts, .concatWith(generateLastLine(boundary, outputMessage.bufferFactory())) .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); + if (logger.isDebugEnabled()) { + body = body.doOnNext(buffer -> Hints.touchDataBuffer(buffer, hints, logger)); + } + return outputMessage.writeWith(body); } From 7418c4b7b70a1af4acb15c82c1205de6d3227991 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Dec 2020 22:28:45 +0000 Subject: [PATCH 0114/1294] Fix buffer leak in AbstractServerHttpResponse See gh-26232 --- .../reactive/AbstractServerHttpResponse.java | 13 +++++++-- .../reactive/ServerHttpResponseTests.java | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index a98b9695b4af..04bd7f2adb92 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -211,9 +211,16 @@ public final Mono writeWith(Publisher body) { // We must resolve value first however, for a chance to handle potential error. if (body instanceof Mono) { return ((Mono) body) - .flatMap(buffer -> doCommit(() -> - writeWithInternal(Mono.fromCallable(() -> buffer) - .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)))) + .flatMap(buffer -> + doCommit(() -> { + try { + return writeWithInternal(Mono.fromCallable(() -> buffer) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)); + } + catch (Throwable ex) { + return Mono.error(ex); + } + }).doOnError(ex -> DataBufferUtils.release(buffer))) .doOnError(t -> getHeaders().clearContentHeaders()); } else { diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index 16c96fc643f3..a12825b5c1a1 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -19,7 +19,9 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Supplier; @@ -27,15 +29,23 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.netty.channel.AbortedException; import reactor.test.StepVerifier; +import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseCookie; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse; import static org.assertj.core.api.Assertions.assertThat; @@ -186,6 +196,25 @@ void beforeCommitErrorShouldLeaveResponseNotCommitted() { }); } + @Test // gh-26232 + void monoResponseShouldNotLeakIfCancelled() { + LeakAwareDataBufferFactory bufferFactory = new LeakAwareDataBufferFactory(); + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerHttpResponse response = new MockServerHttpResponse(bufferFactory); + response.setWriteHandler(flux -> { + throw AbortedException.beforeSend(); + }); + + HttpMessageWriter messageWriter = new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder()); + Mono result = messageWriter.write(Mono.just(Collections.singletonMap("foo", "bar")), + ResolvableType.forClass(Mono.class), ResolvableType.forClass(Map.class), null, + request, response, Collections.emptyMap()); + + StepVerifier.create(result).expectError(AbortedException.class).verify(); + + bufferFactory.checkForLeaks(); + } + private DefaultDataBuffer wrap(String a) { return DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(a.getBytes(StandardCharsets.UTF_8))); From 194cebd7309764652202ce1b8b7fb8ba8953a235 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 7 Dec 2020 22:59:49 +0000 Subject: [PATCH 0115/1294] Upgrade to Reactor 2020.0.2 Closes gh-26176 --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 549cbe26fdfe..db802a1626ff 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ configure(allprojects) { project -> imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.0" mavenBom "io.netty:netty-bom:4.1.54.Final" - mavenBom "io.projectreactor:reactor-bom:2020.0.2-SNAPSHOT" + mavenBom "io.projectreactor:reactor-bom:2020.0.2" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" mavenBom "io.rsocket:rsocket-bom:1.1.0" mavenBom "org.eclipse.jetty:jetty-bom:9.4.35.v20201120" @@ -291,7 +291,6 @@ configure(allprojects) { project -> repositories { mavenCentral() maven { url "https://repo.spring.io/libs-spring-framework-build" } - maven { url "https://repo.spring.io/snapshot" } // reactor } } configurations.all { From e87e03c539b13a709a4019369a396949db3ac4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 8 Dec 2020 00:06:19 +0100 Subject: [PATCH 0116/1294] Refine ConfigurationClassPostProcessor behavior in native images This commit refines ConfigurationClassPostProcessor behavior in native images by skipping configuration classes enhancement instead of raising an error. See spring-projects-experimental/spring-graalvm-native#248 for more details. Closes gh-26236 --- .../context/annotation/ConfigurationClassPostProcessor.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index c1ca0616e4de..449c48fa87d0 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -427,15 +427,11 @@ else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) { configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef); } } - if (configBeanDefs.isEmpty()) { + if (configBeanDefs.isEmpty() || IN_NATIVE_IMAGE) { // nothing to enhance -> return immediately enhanceConfigClasses.end(); return; } - if (IN_NATIVE_IMAGE) { - throw new BeanDefinitionStoreException("@Configuration classes need to be marked as " + - "proxyBeanMethods=false. Found: " + configBeanDefs.keySet()); - } ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer(); for (Map.Entry entry : configBeanDefs.entrySet()) { From 56721669d180d1cc22fc6bd0344873fc19242360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 8 Dec 2020 08:29:21 +0100 Subject: [PATCH 0117/1294] Upgrade to Kotlin 1.4.21 See gh-26132 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index db802a1626ff..8c32debc08ef 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false id 'io.spring.nohttp' version '0.0.5.RELEASE' - id 'org.jetbrains.kotlin.jvm' version '1.4.20' apply false + id 'org.jetbrains.kotlin.jvm' version '1.4.21' apply false id 'org.jetbrains.dokka' version '0.10.1' apply false id 'org.asciidoctor.jvm.convert' version '3.1.0' id 'org.asciidoctor.jvm.pdf' version '3.1.0' @@ -9,7 +9,7 @@ plugins { id "io.freefair.aspectj" version '5.1.1' apply false id "com.github.ben-manes.versions" version '0.28.0' id "me.champeau.gradle.jmh" version "0.5.0" apply false - id "org.jetbrains.kotlin.plugin.serialization" version "1.4.20" apply false + id "org.jetbrains.kotlin.plugin.serialization" version "1.4.21" apply false } ext { @@ -31,7 +31,7 @@ configure(allprojects) { project -> mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" mavenBom "io.rsocket:rsocket-bom:1.1.0" mavenBom "org.eclipse.jetty:jetty-bom:9.4.35.v20201120" - mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.20" + mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.21" mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.2" mavenBom "org.junit:junit-bom:5.7.0" } From 1195b3a0b0b7399d9fd2ea79e1d3c2031a4d8119 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 8 Dec 2020 10:39:56 +0100 Subject: [PATCH 0118/1294] Polishing --- .../aop/aspectj/AspectJProxyUtils.java | 6 ++++-- ...linSerializationJsonHttpMessageConverter.java | 4 ++-- .../json/KotlinSerializationJsonDecoderTests.kt | 3 ++- .../json/KotlinSerializationJsonEncoderTests.kt | 3 ++- ...SerializationJsonHttpMessageConverterTests.kt | 10 ++++++---- .../support/OriginHandshakeInterceptor.java | 16 ++++++++-------- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java index 5c0bc7c998cb..e161007abe98 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -21,6 +21,7 @@ import org.springframework.aop.Advisor; import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -74,7 +75,7 @@ private static boolean isAspectJAdvice(Advisor advisor) { ((PointcutAdvisor) advisor).getPointcut() instanceof AspectJExpressionPointcut)); } - static boolean isVariableName(String name) { + static boolean isVariableName(@Nullable String name) { if (!StringUtils.hasLength(name)) { return false; } @@ -88,4 +89,5 @@ static boolean isVariableName(String name) { } return true; } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java index 7fe280a770c0..7149868217a0 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java @@ -100,9 +100,9 @@ public boolean canRead(Type type, @Nullable Class contextClass, @Nullable Med } @Override - public boolean canWrite(@Nullable Type type, @Nullable Class clazz, @Nullable MediaType mediaType) { + public boolean canWrite(@Nullable Type type, Class clazz, @Nullable MediaType mediaType) { try { - serializer(GenericTypeResolver.resolveType(type, clazz)); + serializer(type != null ? GenericTypeResolver.resolveType(type, clazz) : clazz); return canWrite(mediaType); } catch (Exception ex) { diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt index 2dedc390e33e..f1489814db10 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/json/KotlinSerializationJsonDecoderTests.kt @@ -97,7 +97,8 @@ class KotlinSerializationJsonDecoderTests : AbstractDecoderTests>(), null, MediaType.APPLICATION_JSON)).isTrue() - assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() - assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_JSON)).isTrue() - assertThat(converter.canWrite(typeTokenOf>(), null, MediaType.APPLICATION_PDF)).isFalse() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_JSON)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_PDF)).isFalse() } @Test @@ -297,6 +297,7 @@ class KotlinSerializationJsonHttpMessageConverterTests { assertThat(result).isEqualTo("\"H\u00e9llo W\u00f6rld\"") } + @Serializable @Suppress("ArrayInDataClass") data class SerializableBean( @@ -317,4 +318,5 @@ class KotlinSerializationJsonHttpMessageConverterTests { val superType = base::class.java.genericSuperclass!! return (superType as ParameterizedType).actualTypeArguments.first()!! } + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java index 69bd7833d269..919e2dae8313 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/OriginHandshakeInterceptor.java @@ -19,7 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -31,6 +31,7 @@ import org.springframework.http.server.ServerHttpResponse; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; @@ -84,9 +85,9 @@ public void setAllowedOrigins(Collection allowedOrigins) { * @since 4.1.5 */ public Collection getAllowedOrigins() { - return (this.corsConfiguration.getAllowedOrigins() != null ? - Collections.unmodifiableSet(new HashSet<>(this.corsConfiguration.getAllowedOrigins())) : - Collections.emptyList()); + List allowedOrigins = this.corsConfiguration.getAllowedOrigins(); + return (CollectionUtils.isEmpty(allowedOrigins) ? Collections.emptySet() : + Collections.unmodifiableSet(new LinkedHashSet<>(allowedOrigins))); } /** @@ -104,14 +105,13 @@ public void setAllowedOriginPatterns(Collection allowedOriginPatterns) { /** * Return the allowed {@code Origin} pattern header values. - * * @since 5.3.2 * @see CorsConfiguration#getAllowedOriginPatterns() */ public Collection getAllowedOriginPatterns() { - return (this.corsConfiguration.getAllowedOriginPatterns() != null ? - Collections.unmodifiableSet(new HashSet<>(this.corsConfiguration.getAllowedOriginPatterns())) : - Collections.emptyList()); + List allowedOriginPatterns = this.corsConfiguration.getAllowedOriginPatterns(); + return (CollectionUtils.isEmpty(allowedOriginPatterns) ? Collections.emptySet() : + Collections.unmodifiableSet(new LinkedHashSet<>(allowedOriginPatterns))); } From 3b92d4598425b34fd46223b5b9a0d986e95be6b9 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 8 Dec 2020 11:38:39 +0100 Subject: [PATCH 0119/1294] Upgrade reference docs dependencies This commit upgrades the spring-doc-resources dependency to 0.2.5 and the spring-asciidoctor-extensions-block-switch to 0.5.0 --- gradle/docs.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/docs.gradle b/gradle/docs.gradle index 76ca834b4522..4209b79fdc02 100644 --- a/gradle/docs.gradle +++ b/gradle/docs.gradle @@ -3,7 +3,7 @@ configurations { } dependencies { - asciidoctorExt("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.4.3.RELEASE") + asciidoctorExt("io.spring.asciidoctor:spring-asciidoctor-extensions-block-switch:0.5.0") } repositories { @@ -113,7 +113,7 @@ dokka { } task downloadResources(type: Download) { - def version = "0.2.2.RELEASE" + def version = "0.2.5" src "https://repo.spring.io/release/io/spring/docresources/" + "spring-doc-resources/$version/spring-doc-resources-${version}.zip" dest project.file("$buildDir/docs/spring-doc-resources.zip") From cb2b141d317947f12a88bc47745160d2e9b7c2bc Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 8 Dec 2020 11:59:36 +0100 Subject: [PATCH 0120/1294] Clear path pattern after async result This commit makes sure that the matching pattern attributes is cleared when an async result has been obtained, so that the subsequent dispatch starts from scratch. Closes gh-26239 --- .../function/DefaultAsyncServerResponse.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 0fd283445436..48a5e900e7b6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -40,8 +40,11 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.async.AsyncWebRequest; import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.servlet.ModelAndView; @@ -54,6 +57,16 @@ */ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse implements AsyncServerResponse { + private static final DeferredResultProcessingInterceptor CLEAR_PATTERN_ATTRIBUTE_INTERCEPTOR = + new DeferredResultProcessingInterceptor() { + @Override + public void postProcess(NativeWebRequest request, DeferredResult deferredResult, + Object concurrentResult) { + request.removeAttribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, + RequestAttributes.SCOPE_REQUEST); + } + }; + static final boolean reactiveStreamsPresent = ClassUtils.isPresent( "org.reactivestreams.Publisher", DefaultAsyncServerResponse.class.getClassLoader()); @@ -128,6 +141,7 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.registerDeferredResultInterceptors(CLEAR_PATTERN_ATTRIBUTE_INTERCEPTOR); try { asyncManager.startDeferredResultProcessing(deferredResult); } From 2b77c08e09d978d35cfc85584972ce8a0270fd45 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 8 Dec 2020 13:06:45 +0100 Subject: [PATCH 0121/1294] Clear path pattern in HandlerMapping This commit refactors cb2b141d317947f12a88bc47745160d2e9b7c2bc to move the cleaning code from a DeferredResultProcessingInterceptor to the RouterFunctionMapping. See gh-26239 --- .../function/DefaultAsyncServerResponse.java | 14 -------------- .../function/support/RouterFunctionMapping.java | 7 ++++++- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java index 48a5e900e7b6..0fd283445436 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultAsyncServerResponse.java @@ -40,11 +40,8 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.async.AsyncWebRequest; import org.springframework.web.context.request.async.DeferredResult; -import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; import org.springframework.web.context.request.async.WebAsyncManager; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.servlet.ModelAndView; @@ -57,16 +54,6 @@ */ final class DefaultAsyncServerResponse extends ErrorHandlingServerResponse implements AsyncServerResponse { - private static final DeferredResultProcessingInterceptor CLEAR_PATTERN_ATTRIBUTE_INTERCEPTOR = - new DeferredResultProcessingInterceptor() { - @Override - public void postProcess(NativeWebRequest request, DeferredResult deferredResult, - Object concurrentResult) { - request.removeAttribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, - RequestAttributes.SCOPE_REQUEST); - } - }; - static final boolean reactiveStreamsPresent = ClassUtils.isPresent( "org.reactivestreams.Publisher", DefaultAsyncServerResponse.class.getClassLoader()); @@ -141,7 +128,6 @@ static void writeAsync(HttpServletRequest request, HttpServletResponse response, WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); asyncManager.setAsyncWebRequest(asyncWebRequest); - asyncManager.registerDeferredResultInterceptors(CLEAR_PATTERN_ATTRIBUTE_INTERCEPTOR); try { asyncManager.startDeferredResultProcessing(deferredResult); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java index 3a5cc380802e..040679b595dc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java @@ -189,7 +189,7 @@ private void initMessageConverters() { protected Object getHandlerInternal(HttpServletRequest servletRequest) throws Exception { if (this.routerFunction != null) { ServerRequest request = ServerRequest.create(servletRequest, this.messageConverters); - servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, request); + setAttributes(servletRequest, request); return this.routerFunction.route(request).orElse(null); } else { @@ -197,4 +197,9 @@ protected Object getHandlerInternal(HttpServletRequest servletRequest) throws Ex } } + private void setAttributes(HttpServletRequest servletRequest, ServerRequest request) { + servletRequest.removeAttribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE); + servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, request); + } + } From 07fadae27d87afc09c7e2a90ce7932c97686d1cb Mon Sep 17 00:00:00 2001 From: fengyuanwei Date: Mon, 7 Dec 2020 14:45:40 +0800 Subject: [PATCH 0122/1294] Remove duplicate "property" in PropertyCacheKey.toString() --- .../expression/spel/support/ReflectivePropertyAccessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index fb5c5f9e052e..166b08e7e53f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -628,8 +628,8 @@ public int hashCode() { @Override public String toString() { - return "CacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + ", " + - this.property + ", targetIsClass=" + this.targetIsClass + "]"; + return "CacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + + ", targetIsClass=" + this.targetIsClass + "]"; } @Override From 01fb4dbeba297773f578d264700d6ecc3b56e06b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 8 Dec 2020 14:59:10 +0100 Subject: [PATCH 0123/1294] Polishing See gh-26237 --- .../spel/support/ReflectivePropertyAccessor.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 166b08e7e53f..5fd48cdad88a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -47,7 +47,7 @@ /** * A powerful {@link PropertyAccessor} that uses reflection to access properties - * for reading and possibly also for writing. + * for reading and possibly also for writing on a target instance. * *

    A property can be referenced through a public getter method (when being read) * or a public setter method (when being written), and also as a public field. @@ -98,8 +98,8 @@ public ReflectivePropertyAccessor() { } /** - * Create a new property accessor for reading and possibly writing. - * @param allowWrite whether to also allow for write operations + * Create a new property accessor for reading and possibly also writing. + * @param allowWrite whether to allow write operations on a target instance * @since 4.3.15 * @see #canWrite */ @@ -628,7 +628,7 @@ public int hashCode() { @Override public String toString() { - return "CacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + + return "PropertyCacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + ", targetIsClass=" + this.targetIsClass + "]"; } From cb44ae62e9c49809a85bd44d82e1a5047dfd5290 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 8 Dec 2020 18:43:54 +0000 Subject: [PATCH 0124/1294] Additional DataBuffer hints See gh-26230 --- .../codec/json/AbstractJackson2Encoder.java | 2 ++ .../reactive/AbstractServerHttpResponse.java | 31 +++++++++++++------ .../reactive/ReactorServerHttpResponse.java | 17 ++++++++++ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java index a5bfda5625fd..aa6ac5a26ab6 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Encoder.java @@ -223,6 +223,7 @@ public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, byte[] bytes = byteBuilder.toByteArray(); DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length); buffer.write(bytes); + Hints.touchDataBuffer(buffer, hints, logger); return buffer; } @@ -267,6 +268,7 @@ private DataBuffer encodeStreamingValue(Object value, DataBufferFactory bufferFa DataBuffer buffer = bufferFactory.allocateBuffer(length + separator.length); buffer.write(bytes, offset, length); buffer.write(separator); + Hints.touchDataBuffer(buffer, hints, logger); return buffer; } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 04bd7f2adb92..41b691c83a4e 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -211,16 +211,18 @@ public final Mono writeWith(Publisher body) { // We must resolve value first however, for a chance to handle potential error. if (body instanceof Mono) { return ((Mono) body) - .flatMap(buffer -> - doCommit(() -> { - try { - return writeWithInternal(Mono.fromCallable(() -> buffer) - .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)); - } - catch (Throwable ex) { - return Mono.error(ex); - } - }).doOnError(ex -> DataBufferUtils.release(buffer))) + .flatMap(buffer -> { + touchDataBuffer(buffer); + return doCommit(() -> { + try { + return writeWithInternal(Mono.fromCallable(() -> buffer) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)); + } + catch (Throwable ex) { + return Mono.error(ex); + } + }).doOnError(ex -> DataBufferUtils.release(buffer)); + }) .doOnError(t -> getHeaders().clearContentHeaders()); } else { @@ -323,4 +325,13 @@ else if (this.state.compareAndSet(State.COMMIT_ACTION_FAILED, State.COMMITTING)) */ protected abstract void applyCookies(); + /** + * Allow sub-classes to associate a hint with the data buffer if it is a + * pooled buffer and supports leak tracking. + * @param buffer the buffer to attach a hint to + * @since 5.3.2 + */ + protected void touchDataBuffer(DataBuffer buffer) { + } + } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java index 64b0a1c3f1d7..ccfa1f1f8839 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java @@ -20,6 +20,9 @@ import java.util.List; import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelId; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +30,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -43,6 +47,9 @@ */ class ReactorServerHttpResponse extends AbstractServerHttpResponse implements ZeroCopyHttpOutputMessage { + private static final Log logger = LogFactory.getLog(ReactorServerHttpResponse.class); + + private final HttpServerResponse response; @@ -115,4 +122,14 @@ private Publisher toByteBufs(Publisher dataBuffer Flux.from(dataBuffers).map(NettyDataBufferFactory::toByteBuf); } + @Override + protected void touchDataBuffer(DataBuffer buffer) { + if (logger.isDebugEnabled()) { + this.response.withConnection(connection -> { + ChannelId id = connection.channel().id(); + DataBufferUtils.touch(buffer, "Channel id: " + id.asShortText()); + }); + } + } + } From 25101fb034b1de3da5f602b55ccd57f7081d060e Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 8 Dec 2020 21:33:50 +0000 Subject: [PATCH 0125/1294] Additional fixes for discarding data buffers Closes gh-26232 --- .../http/codec/EncoderHttpMessageWriter.java | 3 +- .../reactive/AbstractServerHttpResponse.java | 30 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java index c429767fbfb7..6c1413ab4f81 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java @@ -132,7 +132,8 @@ public Mono write(Publisher inputStream, ResolvableType eleme message.getHeaders().setContentLength(buffer.readableByteCount()); return message.writeWith(Mono.just(buffer) .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)); - }); + }) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); } if (isStreamingMediaType(contentType)) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 41b691c83a4e..1feaaa5ca4a8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -213,17 +213,27 @@ public final Mono writeWith(Publisher body) { return ((Mono) body) .flatMap(buffer -> { touchDataBuffer(buffer); - return doCommit(() -> { - try { - return writeWithInternal(Mono.fromCallable(() -> buffer) - .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)); - } - catch (Throwable ex) { - return Mono.error(ex); - } - }).doOnError(ex -> DataBufferUtils.release(buffer)); + AtomicReference subscribed = new AtomicReference<>(false); + return doCommit( + () -> { + try { + return writeWithInternal(Mono.fromCallable(() -> buffer) + .doOnSubscribe(s -> subscribed.set(true)) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)); + } + catch (Throwable ex) { + return Mono.error(ex); + } + }) + .doOnError(ex -> DataBufferUtils.release(buffer)) + .doOnCancel(() -> { + if (!subscribed.get()) { + DataBufferUtils.release(buffer); + } + }); }) - .doOnError(t -> getHeaders().clearContentHeaders()); + .doOnError(t -> getHeaders().clearContentHeaders()) + .doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release); } else { return new ChannelSendOperator<>(body, inner -> doCommit(() -> writeWithInternal(inner))) From 06e352822abc1eb0f5858ca9e79e38f8958e3c63 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Wed, 9 Dec 2020 06:11:23 +0000 Subject: [PATCH 0126/1294] Next development version (v5.3.3-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 53d0643b4525..432f21d52d5e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.3.2-SNAPSHOT +version=5.3.3-SNAPSHOT org.gradle.jvmargs=-Xmx1536M org.gradle.caching=true org.gradle.parallel=true From 2a47751fcd09f0db0a46fa00f1e089744073c966 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 10 Dec 2020 16:24:32 +0100 Subject: [PATCH 0127/1294] Defensively handle loadClass null result in BeanUtils.findEditorByConvention Closes gh-26252 --- .../org/springframework/beans/BeanUtils.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 3980a24dd719..cabe6d27c285 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -543,6 +543,7 @@ public static PropertyEditor findEditorByConvention(@Nullable Class targetTyp if (targetType == null || targetType.isArray() || unknownEditorTypes.contains(targetType)) { return null; } + ClassLoader cl = targetType.getClassLoader(); if (cl == null) { try { @@ -559,28 +560,34 @@ public static PropertyEditor findEditorByConvention(@Nullable Class targetTyp return null; } } + String targetTypeName = targetType.getName(); String editorName = targetTypeName + "Editor"; try { Class editorClass = cl.loadClass(editorName); - if (!PropertyEditor.class.isAssignableFrom(editorClass)) { - if (logger.isInfoEnabled()) { - logger.info("Editor class [" + editorName + - "] does not implement [java.beans.PropertyEditor] interface"); + if (editorClass != null) { + if (!PropertyEditor.class.isAssignableFrom(editorClass)) { + if (logger.isInfoEnabled()) { + logger.info("Editor class [" + editorName + + "] does not implement [java.beans.PropertyEditor] interface"); + } + unknownEditorTypes.add(targetType); + return null; } - unknownEditorTypes.add(targetType); - return null; + return (PropertyEditor) instantiateClass(editorClass); } - return (PropertyEditor) instantiateClass(editorClass); + // Misbehaving ClassLoader returned null instead of ClassNotFoundException + // - fall back to unknown editor type registration below } catch (ClassNotFoundException ex) { - if (logger.isTraceEnabled()) { - logger.trace("No property editor [" + editorName + "] found for type " + - targetTypeName + " according to 'Editor' suffix convention"); - } - unknownEditorTypes.add(targetType); - return null; + // Ignore - fall back to unknown editor type registration below } + if (logger.isTraceEnabled()) { + logger.trace("No property editor [" + editorName + "] found for type " + + targetTypeName + " according to 'Editor' suffix convention"); + } + unknownEditorTypes.add(targetType); + return null; } /** From 9d124fffcbe1a4167a251b267bf2cc763bb2d3ec Mon Sep 17 00:00:00 2001 From: alexscari <42527357+Alexscari7@users.noreply.github.com> Date: Fri, 11 Dec 2020 00:32:58 +0800 Subject: [PATCH 0128/1294] Fix typo in Javadoc for AbstractJdbcCall Closes gh-26254 --- .../org/springframework/jdbc/core/simple/AbstractJdbcCall.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java index ddfdd589a9ee..edebf338c114 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java @@ -433,7 +433,7 @@ protected List getCallParameters() { /** * Match the provided in parameter values with registered parameters and * parameters defined via meta-data processing. - * @param parameterSource the parameter vakues provided as a {@link SqlParameterSource} + * @param parameterSource the parameter values provided as a {@link SqlParameterSource} * @return a Map with parameter names and values */ protected Map matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) { From 721dacca5ae49afadf5f9801dbe673594bc99757 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 11 Dec 2020 09:45:21 +0100 Subject: [PATCH 0129/1294] Upgrade JDK8, JDK11 and JDK15 versions in CI build --- ci/images/get-jdk-url.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 25a9daea4cdb..877d88be3a2b 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -3,16 +3,16 @@ set -e case "$1" in java8) - echo "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u265-b01/OpenJDK8U-jdk_x64_linux_hotspot_8u265b01.tar.gz" + echo "https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u275-b01/OpenJDK8U-jdk_x64_linux_hotspot_8u275b01.tar.gz" ;; java11) - echo "https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.8%2B10/OpenJDK11U-jdk_x64_linux_hotspot_11.0.8_10.tar.gz" + echo "https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.9.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.9.1_1.tar.gz" ;; java14) echo "https://github.com/AdoptOpenJDK/openjdk14-binaries/releases/download/jdk-14.0.2%2B12/OpenJDK14U-jdk_x64_linux_hotspot_14.0.2_12.tar.gz" ;; java15) - echo "https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15%2B36/OpenJDK15U-jdk_x64_linux_hotspot_15_36.tar.gz" + echo "https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15.0.1%2B9/OpenJDK15U-jdk_x64_linux_hotspot_15.0.1_9.tar.gz" ;; *) echo $"Unknown java version" From 4597e9b5476770cb2dd301886b6c3b70d9963677 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 11 Dec 2020 09:46:00 +0100 Subject: [PATCH 0130/1294] Upgrade CI container images to Ubuntu Focal --- ci/images/setup.sh | 5 ++++- ci/images/spring-framework-ci-image/Dockerfile | 2 +- ci/images/spring-framework-jdk11-ci-image/Dockerfile | 2 +- ci/images/spring-framework-jdk14-ci-image/Dockerfile | 2 +- ci/images/spring-framework-jdk15-ci-image/Dockerfile | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ci/images/setup.sh b/ci/images/setup.sh index ffc3d86d89d3..9942d5acc127 100755 --- a/ci/images/setup.sh +++ b/ci/images/setup.sh @@ -5,8 +5,11 @@ set -ex # UTILS ########################################################### +export DEBIAN_FRONTEND=noninteractive apt-get update -apt-get install --no-install-recommends -y ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig +apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig +ln -fs /usr/share/zoneinfo/UTC /etc/localtime +dpkg-reconfigure --frontend noninteractive tzdata rm -rf /var/lib/apt/lists/* curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.3/concourse-java.sh > /opt/concourse-java.sh diff --git a/ci/images/spring-framework-ci-image/Dockerfile b/ci/images/spring-framework-ci-image/Dockerfile index 2e237054650b..0ea1b94f7dfd 100644 --- a/ci/images/spring-framework-ci-image/Dockerfile +++ b/ci/images/spring-framework-ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic-20200713 +FROM ubuntu:focal-20201106 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh diff --git a/ci/images/spring-framework-jdk11-ci-image/Dockerfile b/ci/images/spring-framework-jdk11-ci-image/Dockerfile index 29ac39677553..203f56151fc8 100644 --- a/ci/images/spring-framework-jdk11-ci-image/Dockerfile +++ b/ci/images/spring-framework-jdk11-ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic-20200713 +FROM ubuntu:focal-20201106 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh diff --git a/ci/images/spring-framework-jdk14-ci-image/Dockerfile b/ci/images/spring-framework-jdk14-ci-image/Dockerfile index bd7a467de227..3759b670648a 100644 --- a/ci/images/spring-framework-jdk14-ci-image/Dockerfile +++ b/ci/images/spring-framework-jdk14-ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic-20200713 +FROM ubuntu:focal-20201106 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh diff --git a/ci/images/spring-framework-jdk15-ci-image/Dockerfile b/ci/images/spring-framework-jdk15-ci-image/Dockerfile index c3b544e01cca..2398bf069931 100644 --- a/ci/images/spring-framework-jdk15-ci-image/Dockerfile +++ b/ci/images/spring-framework-jdk15-ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:bionic-20200713 +FROM ubuntu:focal-20201106 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh From 657641ebaac4d6b01cd20918096f450a86ab28f7 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 11 Dec 2020 11:08:33 +0100 Subject: [PATCH 0131/1294] Remove JDK14 CI variant from build pipeline --- ci/images/get-jdk-url.sh | 3 -- .../Dockerfile | 8 ---- ci/pipeline.yml | 48 +------------------ 3 files changed, 1 insertion(+), 58 deletions(-) delete mode 100644 ci/images/spring-framework-jdk14-ci-image/Dockerfile diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 877d88be3a2b..51641988bc86 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -8,9 +8,6 @@ case "$1" in java11) echo "https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.9.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.9.1_1.tar.gz" ;; - java14) - echo "https://github.com/AdoptOpenJDK/openjdk14-binaries/releases/download/jdk-14.0.2%2B12/OpenJDK14U-jdk_x64_linux_hotspot_14.0.2_12.tar.gz" - ;; java15) echo "https://github.com/AdoptOpenJDK/openjdk15-binaries/releases/download/jdk-15.0.1%2B9/OpenJDK15U-jdk_x64_linux_hotspot_15.0.1_9.tar.gz" ;; diff --git a/ci/images/spring-framework-jdk14-ci-image/Dockerfile b/ci/images/spring-framework-jdk14-ci-image/Dockerfile deleted file mode 100644 index 3759b670648a..000000000000 --- a/ci/images/spring-framework-jdk14-ci-image/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM ubuntu:focal-20201106 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh java14 - -ENV JAVA_HOME /opt/openjdk -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 7e9270ff48df..7bfae68bd028 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -87,12 +87,6 @@ resources: source: <<: *docker-resource-source repository: ((docker-hub-organization))/spring-framework-jdk11-ci-image -- name: spring-framework-jdk14-ci-image - type: docker-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-jdk14-ci-image - name: spring-framework-jdk15-ci-image type: docker-image icon: docker @@ -123,14 +117,6 @@ resources: access_token: ((github-ci-status-token)) branch: ((branch)) context: jdk11-build -- name: repo-status-jdk14-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: jdk14-build - name: repo-status-jdk15-build type: github-status-resource icon: eye-check-outline @@ -176,10 +162,6 @@ jobs: params: build: ci-images-git-repo/ci/images dockerfile: ci-images-git-repo/ci/images/spring-framework-jdk11-ci-image/Dockerfile - - put: spring-framework-jdk14-ci-image - params: - build: ci-images-git-repo/ci/images - dockerfile: ci-images-git-repo/ci/images/spring-framework-jdk14-ci-image/Dockerfile - put: spring-framework-jdk15-ci-image params: build: ci-images-git-repo/ci/images @@ -268,34 +250,6 @@ jobs: <<: *slack-fail-params - put: repo-status-jdk11-build params: { state: "success", commit: "git-repo" } -- name: jdk14-build - serial: true - public: true - plan: - - get: spring-framework-jdk14-ci-image - - get: git-repo - - get: every-morning - trigger: true - - put: repo-status-jdk14-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: check-project - privileged: true - timeout: ((task-timeout)) - image: spring-framework-jdk14-ci-image - file: git-repo/ci/tasks/check-project.yml - params: - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params - on_failure: - do: - - put: repo-status-jdk14-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-jdk14-build - params: { state: "success", commit: "git-repo" } - name: jdk15-build serial: true public: true @@ -480,7 +434,7 @@ jobs: groups: - name: "builds" - jobs: ["build", "jdk11-build", "jdk14-build", "jdk15-build"] + jobs: ["build", "jdk11-build", "jdk15-build"] - name: "releases" jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone","promote-rc", "promote-release", "sync-to-maven-central"] - name: "ci-images" From ec33b4241adcfe98a565c5fb12603bd8b5cff725 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 11 Dec 2020 11:53:53 +0100 Subject: [PATCH 0132/1294] Upgrade to Netty 4.1.55, Tomcat 9.0.41, Caffeine 2.8.8 --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 8c32debc08ef..22d4def6ebba 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ configure(allprojects) { project -> dependencyManagement { imports { mavenBom "com.fasterxml.jackson:jackson-bom:2.12.0" - mavenBom "io.netty:netty-bom:4.1.54.Final" + mavenBom "io.netty:netty-bom:4.1.55.Final" mavenBom "io.projectreactor:reactor-bom:2020.0.2" mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8" mavenBom "io.rsocket:rsocket-bom:1.1.0" @@ -95,7 +95,7 @@ configure(allprojects) { project -> } dependency "com.h2database:h2:1.4.200" - dependency "com.github.ben-manes.caffeine:caffeine:2.8.6" + dependency "com.github.ben-manes.caffeine:caffeine:2.8.8" dependency "com.github.librepdf:openpdf:1.3.23" dependency "com.rometools:rome:1.15.0" dependency "commons-io:commons-io:2.5" @@ -128,14 +128,14 @@ configure(allprojects) { project -> dependency "org.webjars:webjars-locator-core:0.46" dependency "org.webjars:underscorejs:1.8.3" - dependencySet(group: 'org.apache.tomcat', version: '9.0.40') { + dependencySet(group: 'org.apache.tomcat', version: '9.0.41') { entry 'tomcat-util' entry('tomcat-websocket') { exclude group: "org.apache.tomcat", name: "tomcat-websocket-api" exclude group: "org.apache.tomcat", name: "tomcat-servlet-api" } } - dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.40') { + dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.41') { entry 'tomcat-embed-core' entry 'tomcat-embed-websocket' } From 17e6cf1cc1b57bd65bfae8da8cddc8314a89408d Mon Sep 17 00:00:00 2001 From: izeye Date: Sat, 12 Dec 2020 11:33:11 +0900 Subject: [PATCH 0133/1294] Replace AtomicReference with AtomicBoolean in AbstractServerHttpResponse.writeWith() --- .../http/server/reactive/AbstractServerHttpResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java index 1feaaa5ca4a8..5a0ee5aba56d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -213,7 +214,7 @@ public final Mono writeWith(Publisher body) { return ((Mono) body) .flatMap(buffer -> { touchDataBuffer(buffer); - AtomicReference subscribed = new AtomicReference<>(false); + AtomicBoolean subscribed = new AtomicBoolean(); return doCommit( () -> { try { From bcfbde984894ea198124ac60a3558ca341cfc495 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 14 Dec 2020 21:13:01 +0000 Subject: [PATCH 0134/1294] Parse parts in MockMultipartHttpServletRequestBuilder Closes gh-26261 --- .../web/servlet/TestDispatcherServlet.java | 5 -- ...ockMultipartHttpServletRequestBuilder.java | 56 +++++++++++++------ ...ltipartHttpServletRequestBuilderTests.java | 39 +++++++++---- 3 files changed, 67 insertions(+), 33 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java b/spring-test/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java index 041cf24f6e10..8c9a4f9fb7ac 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java @@ -35,7 +35,6 @@ import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; import org.springframework.web.context.request.async.WebAsyncUtils; -import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.ModelAndView; @@ -68,10 +67,6 @@ public TestDispatcherServlet(WebApplicationContext webApplicationContext) { protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - if (!request.getParts().isEmpty()) { - request = new StandardMultipartHttpServletRequest(request); - } - registerAsyncResultInterceptors(request); super.service(request, response); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java index 75cb972331be..c0f15fcc81c8 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java @@ -17,7 +17,10 @@ package org.springframework.test.web.servlet.request; import java.io.IOException; +import java.io.InputStreamReader; import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -25,17 +28,17 @@ import javax.servlet.ServletContext; import javax.servlet.http.Part; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartHttpServletRequest; -import org.springframework.mock.web.MockPart; import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; /** * Default builder for {@link MockMultipartHttpServletRequest}. @@ -141,26 +144,47 @@ public Object merge(@Nullable Object parent) { @Override protected final MockHttpServletRequest createServletRequest(ServletContext servletContext) { MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(servletContext); - this.files.forEach(file -> request.addPart(toMockPart(file))); - this.parts.values().stream().flatMap(Collection::stream).forEach(request::addPart); - return request; - } - - private MockPart toMockPart(MockMultipartFile file) { - byte[] bytes = null; - if (!file.isEmpty()) { + this.files.forEach(request::addFile); + this.parts.values().stream().flatMap(Collection::stream).forEach(part -> { + request.addPart(part); try { - bytes = file.getBytes(); + MultipartFile file = asMultipartFile(part); + if (file != null) { + request.addFile(file); + return; + } + String value = toParameterValue(part); + if (value != null) { + request.addParameter(part.getName(), toParameterValue(part)); + } } catch (IOException ex) { - throw new IllegalStateException("Unexpected IOException", ex); + throw new IllegalStateException("Failed to read content for part " + part.getName(), ex); } + }); + return request; + } + + @Nullable + private MultipartFile asMultipartFile(Part part) throws IOException { + String name = part.getName(); + String filename = part.getSubmittedFileName(); + if (filename != null) { + return new MockMultipartFile(name, filename, part.getContentType(), part.getInputStream()); } - MockPart part = new MockPart(file.getName(), file.getOriginalFilename(), bytes); - if (file.getContentType() != null) { - part.getHeaders().set(HttpHeaders.CONTENT_TYPE, file.getContentType()); + return null; + } + + @Nullable + private String toParameterValue(Part part) throws IOException { + String rawType = part.getContentType(); + MediaType mediaType = (rawType != null ? MediaType.parseMediaType(rawType) : MediaType.TEXT_PLAIN); + if (!mediaType.isCompatibleWith(MediaType.TEXT_PLAIN)) { + return null; } - return part; + Charset charset = (mediaType.getCharset() != null ? mediaType.getCharset() : StandardCharsets.UTF_8); + InputStreamReader reader = new InputStreamReader(part.getInputStream(), charset); + return FileCopyUtils.copyToString(reader); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilderTests.java index a7087f424fb0..b1414d2bf4c2 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilderTests.java @@ -16,19 +16,19 @@ package org.springframework.test.web.servlet.request; -import java.nio.charset.StandardCharsets; - import javax.servlet.http.Part; import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockMultipartHttpServletRequest; import org.springframework.mock.web.MockPart; import org.springframework.mock.web.MockServletContext; -import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; /** @@ -38,17 +38,32 @@ public class MockMultipartHttpServletRequestBuilderTests { @Test // gh-26166 - void addFilesAndParts() throws Exception { - MockHttpServletRequest mockRequest = new MockMultipartHttpServletRequestBuilder("/upload") - .file(new MockMultipartFile("file", "test.txt", "text/plain", "Test".getBytes(StandardCharsets.UTF_8))) - .part(new MockPart("data", "{\"node\":\"node\"}".getBytes(StandardCharsets.UTF_8))) - .buildRequest(new MockServletContext()); + void addFileAndParts() throws Exception { + MockMultipartHttpServletRequest mockRequest = + (MockMultipartHttpServletRequest) new MockMultipartHttpServletRequestBuilder("/upload") + .file(new MockMultipartFile("file", "test.txt", "text/plain", "Test".getBytes(UTF_8))) + .part(new MockPart("name", "value".getBytes(UTF_8))) + .buildRequest(new MockServletContext()); + + assertThat(mockRequest.getFileMap()).containsOnlyKeys("file"); + assertThat(mockRequest.getParameterMap()).containsOnlyKeys("name"); + assertThat(mockRequest.getParts()).extracting(Part::getName).containsExactly("name"); + } + + @Test // gh-26261 + void addFileWithoutFilename() throws Exception { + MockPart jsonPart = new MockPart("data", "{\"node\":\"node\"}".getBytes(UTF_8)); + jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON); - StandardMultipartHttpServletRequest parsedRequest = new StandardMultipartHttpServletRequest(mockRequest); + MockMultipartHttpServletRequest mockRequest = + (MockMultipartHttpServletRequest) new MockMultipartHttpServletRequestBuilder("/upload") + .file(new MockMultipartFile("file", "Test".getBytes(UTF_8))) + .part(jsonPart) + .buildRequest(new MockServletContext()); - assertThat(parsedRequest.getParameterMap()).containsOnlyKeys("data"); - assertThat(parsedRequest.getFileMap()).containsOnlyKeys("file"); - assertThat(parsedRequest.getParts()).extracting(Part::getName).containsExactly("file", "data"); + assertThat(mockRequest.getFileMap()).containsOnlyKeys("file"); + assertThat(mockRequest.getParameterMap()).isEmpty(); + assertThat(mockRequest.getParts()).extracting(Part::getName).containsExactly("data"); } @Test From 83c19cd60ee95dd315237c24f1fcfd358e58d53b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 15 Dec 2020 13:23:53 +0100 Subject: [PATCH 0135/1294] Fix NPE when calling NettyHeadersAdapter.add() Prior to this commit, the `NettyHeadersAdapter` would directly delegate the `add()` and `set()` calls to the adapted `io.netty.handler.codec.http.HttpHeaders`. This implementation rejects `null` values with exceptions. This commit aligns the behavior here with other implementations, by not rejecting null values but simply ignoring them. Fixes gh-26274 --- .../http/client/reactive/NettyHeadersAdapter.java | 8 ++++++-- .../http/server/reactive/NettyHeadersAdapter.java | 8 ++++++-- .../http/server/reactive/HeadersAdaptersTests.java | 8 ++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/NettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/client/reactive/NettyHeadersAdapter.java index c0c92750eb0e..603a3e7380df 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/NettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/NettyHeadersAdapter.java @@ -56,7 +56,9 @@ public String getFirst(String key) { @Override public void add(String key, @Nullable String value) { - this.headers.add(key, value); + if (value != null) { + this.headers.add(key, value); + } } @Override @@ -71,7 +73,9 @@ public void addAll(MultiValueMap values) { @Override public void set(String key, @Nullable String value) { - this.headers.set(key, value); + if (value != null) { + this.headers.set(key, value); + } } @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/NettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/NettyHeadersAdapter.java index 4d96585fda9e..dceddb7fb3af 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/NettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/NettyHeadersAdapter.java @@ -56,7 +56,9 @@ public String getFirst(String key) { @Override public void add(String key, @Nullable String value) { - this.headers.add(key, value); + if (value != null) { + this.headers.add(key, value); + } } @Override @@ -71,7 +73,9 @@ public void addAll(MultiValueMap values) { @Override public void set(String key, @Nullable String value) { - this.headers.set(key, value); + if (value != null) { + this.headers.set(key, value); + } } @Override diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/HeadersAdaptersTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/HeadersAdaptersTests.java index ba0deea0179d..1b7d34290b67 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/HeadersAdaptersTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/HeadersAdaptersTests.java @@ -94,6 +94,14 @@ void putShouldOverrideExisting(String displayName, MultiValueMap assertThat(headers.get("TestHeader").size()).isEqualTo(1); } + @ParameterizedHeadersTest + void nullValuesShouldNotFail(String displayName, MultiValueMap headers) { + headers.add("TestHeader", null); + assertThat(headers.getFirst("TestHeader")).isNull(); + headers.set("TestHeader", null); + assertThat(headers.getFirst("TestHeader")).isNull(); + } + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @ParameterizedTest(name = "[{index}] {0}") From a11d1c8510c34cdb0889a13af969c3cf424464f3 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 15 Dec 2020 21:33:50 +0000 Subject: [PATCH 0136/1294] Enrich WebSocketHandler context Closes gh-26210 --- .../adapter/ContextWebSocketHandler.java | 64 +++++++++++++++++++ .../socket/client/JettyWebSocketClient.java | 31 +++++---- .../client/StandardWebSocketClient.java | 21 ++++-- .../client/UndertowWebSocketClient.java | 18 +++--- .../upgrade/JettyRequestUpgradeStrategy.java | 16 +++-- .../upgrade/TomcatRequestUpgradeStrategy.java | 29 +++++---- .../UndertowRequestUpgradeStrategy.java | 17 +++-- .../AbstractWebSocketIntegrationTests.java | 7 ++ .../socket/WebSocketIntegrationTests.java | 8 ++- 9 files changed, 162 insertions(+), 49 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/ContextWebSocketHandler.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/ContextWebSocketHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/ContextWebSocketHandler.java new file mode 100644 index 000000000000..816dc147bd57 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/ContextWebSocketHandler.java @@ -0,0 +1,64 @@ +/* + * 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.socket.adapter; + +import java.util.List; + +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketSession; + +/** + * {@link WebSocketHandler} decorator that enriches the context of the target handler. + * + * @author Rossen Stoyanchev + * @since 5.3.3 + */ +public final class ContextWebSocketHandler implements WebSocketHandler { + + private final WebSocketHandler delegate; + + private final ContextView contextView; + + + private ContextWebSocketHandler(WebSocketHandler delegate, ContextView contextView) { + this.delegate = delegate; + this.contextView = contextView; + } + + + @Override + public List getSubProtocols() { + return this.delegate.getSubProtocols(); + } + + @Override + public Mono handle(WebSocketSession session) { + return this.delegate.handle(session).contextWrite(this.contextView); + } + + + /** + * Return the given handler, decorated to insert the given context, or the + * same handler instance when the context is empty. + */ + public static WebSocketHandler decorate(WebSocketHandler handler, ContextView contextView) { + return (!contextView.isEmpty() ? new ContextWebSocketHandler(handler, contextView) : handler); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index 0dbe7b0ba164..1074837cbeb2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.socket.client; +import java.io.IOException; import java.net.URI; import org.apache.commons.logging.Log; @@ -33,6 +34,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; @@ -137,18 +139,23 @@ public Mono execute(URI url, HttpHeaders headers, WebSocketHandler handler private Mono executeInternal(URI url, HttpHeaders headers, WebSocketHandler handler) { Sinks.Empty completionSink = Sinks.empty(); - return Mono.fromCallable( - () -> { - if (logger.isDebugEnabled()) { - logger.debug("Connecting to " + url); - } - Object jettyHandler = createHandler(url, handler, completionSink); - ClientUpgradeRequest request = new ClientUpgradeRequest(); - request.setSubProtocols(handler.getSubProtocols()); - UpgradeListener upgradeListener = new DefaultUpgradeListener(headers); - return this.jettyClient.connect(jettyHandler, url, request, upgradeListener); - }) - .then(completionSink.asMono()); + return Mono.deferContextual(contextView -> { + if (logger.isDebugEnabled()) { + logger.debug("Connecting to " + url); + } + Object jettyHandler = createHandler( + url, ContextWebSocketHandler.decorate(handler, contextView), completionSink); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setSubProtocols(handler.getSubProtocols()); + UpgradeListener upgradeListener = new DefaultUpgradeListener(headers); + try { + this.jettyClient.connect(jettyHandler, url, request, upgradeListener); + return completionSink.asMono(); + } + catch (IOException ex) { + return Mono.error(ex); + } + }); } private Object createHandler(URI url, WebSocketHandler handler, Sinks.Empty completion) { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java index 0ce692278986..4bf98585a059 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java @@ -39,6 +39,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; import org.springframework.web.reactive.socket.adapter.StandardWebSocketHandlerAdapter; import org.springframework.web.reactive.socket.adapter.StandardWebSocketSession; @@ -95,20 +96,26 @@ public Mono execute(URI url, HttpHeaders headers, WebSocketHandler handler } private Mono executeInternal(URI url, HttpHeaders requestHeaders, WebSocketHandler handler) { - Sinks.Empty completionSink = Sinks.empty(); - return Mono.fromCallable( - () -> { + Sinks.Empty completion = Sinks.empty(); + return Mono.deferContextual( + contextView -> { if (logger.isDebugEnabled()) { logger.debug("Connecting to " + url); } List protocols = handler.getSubProtocols(); DefaultConfigurator configurator = new DefaultConfigurator(requestHeaders); - Endpoint endpoint = createEndpoint(url, handler, completionSink, configurator); + Endpoint endpoint = createEndpoint( + url, ContextWebSocketHandler.decorate(handler, contextView), completion, configurator); ClientEndpointConfig config = createEndpointConfig(configurator, protocols); - return this.webSocketContainer.connectToServer(endpoint, config, url); + try { + this.webSocketContainer.connectToServer(endpoint, config, url); + return completion.asMono(); + } + catch (Exception ex) { + return Mono.error(ex); + } }) - .subscribeOn(Schedulers.boundedElastic()) // connectToServer is blocking - .then(completionSink.asMono()); + .subscribeOn(Schedulers.boundedElastic()); // connectToServer is blocking } private StandardWebSocketHandlerAdapter createEndpoint(URI url, WebSocketHandler handler, diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/UndertowWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/UndertowWebSocketClient.java index 03563efdc806..272e04f345d5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/UndertowWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/UndertowWebSocketClient.java @@ -42,6 +42,7 @@ import org.springframework.util.Assert; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; import org.springframework.web.reactive.socket.adapter.UndertowWebSocketHandlerAdapter; import org.springframework.web.reactive.socket.adapter.UndertowWebSocketSession; @@ -154,9 +155,9 @@ public Mono execute(URI url, HttpHeaders headers, WebSocketHandler handler } private Mono executeInternal(URI url, HttpHeaders headers, WebSocketHandler handler) { - Sinks.Empty completionSink = Sinks.empty(); - return Mono.fromCallable( - () -> { + Sinks.Empty completion = Sinks.empty(); + return Mono.deferContextual( + contextView -> { if (logger.isDebugEnabled()) { logger.debug("Connecting to " + url); } @@ -164,21 +165,22 @@ private Mono executeInternal(URI url, HttpHeaders headers, WebSocketHandle ConnectionBuilder builder = createConnectionBuilder(url); DefaultNegotiation negotiation = new DefaultNegotiation(protocols, headers, builder); builder.setClientNegotiation(negotiation); - return builder.connect().addNotifier( + builder.connect().addNotifier( new IoFuture.HandlingNotifier() { @Override public void handleDone(WebSocketChannel channel, Object attachment) { - handleChannel(url, handler, completionSink, negotiation, channel); + handleChannel(url, ContextWebSocketHandler.decorate(handler, contextView), + completion, negotiation, channel); } @Override public void handleFailed(IOException ex, Object attachment) { // Ignore result: can't overflow, ok if not first or no one listens - completionSink.tryEmitError( + completion.tryEmitError( new IllegalStateException("Failed to connect to " + url, ex)); } }, null); - }) - .then(completionSink.asMono()); + return completion.asMono(); + }); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java index f4a4882e7bea..7d5b00810060 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/JettyRequestUpgradeStrategy.java @@ -16,6 +16,7 @@ package org.springframework.web.reactive.socket.server.upgrade; +import java.io.IOException; import java.util.function.Supplier; import javax.servlet.ServletContext; @@ -39,6 +40,7 @@ import org.springframework.util.Assert; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; import org.springframework.web.reactive.socket.adapter.JettyWebSocketHandlerAdapter; import org.springframework.web.reactive.socket.adapter.JettyWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; @@ -152,9 +154,6 @@ public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler, HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); DataBufferFactory factory = response.bufferFactory(); - JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( - handler, session -> new JettyWebSocketSession(session, handshakeInfo, factory)); - startLazily(servletRequest); Assert.state(this.factory != null, "No WebSocketServerFactory available"); @@ -163,15 +162,22 @@ public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler, // Trigger WebFlux preCommit actions and upgrade return exchange.getResponse().setComplete() - .then(Mono.fromCallable(() -> { + .then(Mono.deferContextual(contextView -> { + JettyWebSocketHandlerAdapter adapter = new JettyWebSocketHandlerAdapter( + ContextWebSocketHandler.decorate(handler, contextView), + session -> new JettyWebSocketSession(session, handshakeInfo, factory)); + try { adapterHolder.set(new WebSocketHandlerContainer(adapter, subProtocol)); this.factory.acceptWebSocket(servletRequest, servletResponse); } + catch (IOException ex) { + return Mono.error(ex); + } finally { adapterHolder.remove(); } - return null; + return Mono.empty(); })); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java index ad56c0370f10..a9d84b16449a 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/TomcatRequestUpgradeStrategy.java @@ -38,6 +38,7 @@ import org.springframework.util.Assert; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; import org.springframework.web.reactive.socket.adapter.StandardWebSocketHandlerAdapter; import org.springframework.web.reactive.socket.adapter.TomcatWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; @@ -137,20 +138,26 @@ public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler, HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); DataBufferFactory bufferFactory = response.bufferFactory(); - Endpoint endpoint = new StandardWebSocketHandlerAdapter( - handler, session -> new TomcatWebSocketSession(session, handshakeInfo, bufferFactory)); - - String requestURI = servletRequest.getRequestURI(); - DefaultServerEndpointConfig config = new DefaultServerEndpointConfig(requestURI, endpoint); - config.setSubprotocols(subProtocol != null ? - Collections.singletonList(subProtocol) : Collections.emptyList()); - // Trigger WebFlux preCommit actions and upgrade return exchange.getResponse().setComplete() - .then(Mono.fromCallable(() -> { + .then(Mono.deferContextual(contextView -> { + Endpoint endpoint = new StandardWebSocketHandlerAdapter( + ContextWebSocketHandler.decorate(handler, contextView), + session -> new TomcatWebSocketSession(session, handshakeInfo, bufferFactory)); + + String requestURI = servletRequest.getRequestURI(); + DefaultServerEndpointConfig config = new DefaultServerEndpointConfig(requestURI, endpoint); + config.setSubprotocols(subProtocol != null ? + Collections.singletonList(subProtocol) : Collections.emptyList()); + WsServerContainer container = getContainer(servletRequest); - container.doUpgrade(servletRequest, servletResponse, config, Collections.emptyMap()); - return null; + try { + container.doUpgrade(servletRequest, servletResponse, config, Collections.emptyMap()); + } + catch (Exception ex) { + return Mono.error(ex); + } + return Mono.empty(); })); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java index d57fb1b9481c..fa60bcdb715f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/upgrade/UndertowRequestUpgradeStrategy.java @@ -37,6 +37,7 @@ import org.springframework.lang.Nullable; import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.adapter.ContextWebSocketHandler; import org.springframework.web.reactive.socket.adapter.UndertowWebSocketHandlerAdapter; import org.springframework.web.reactive.socket.adapter.UndertowWebSocketSession; import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; @@ -67,10 +68,18 @@ public Mono upgrade(ServerWebExchange exchange, WebSocketHandler handler, // Trigger WebFlux preCommit actions and upgrade return exchange.getResponse().setComplete() - .then(Mono.fromCallable(() -> { - DefaultCallback callback = new DefaultCallback(handshakeInfo, handler, bufferFactory); - new WebSocketProtocolHandshakeHandler(handshakes, callback).handleRequest(httpExchange); - return null; + .then(Mono.deferContextual(contextView -> { + DefaultCallback callback = new DefaultCallback( + handshakeInfo, + ContextWebSocketHandler.decorate(handler, contextView), + bufferFactory); + try { + new WebSocketProtocolHandshakeHandler(handshakes, callback).handleRequest(httpExchange); + } + catch (Exception ex) { + return Mono.error(ex); + } + return Mono.empty(); })); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractWebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractWebSocketIntegrationTests.java index 8d0e78cf2a4b..494107f6c9d2 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractWebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/AbstractWebSocketIntegrationTests.java @@ -43,6 +43,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.socket.client.JettyWebSocketClient; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; @@ -57,6 +58,7 @@ import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy; import org.springframework.web.reactive.socket.server.upgrade.UndertowRequestUpgradeStrategy; +import org.springframework.web.server.WebFilter; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; import org.springframework.web.testfixture.http.server.reactive.bootstrap.JettyHttpServer; @@ -165,6 +167,11 @@ protected URI getUrl(String path) { @Configuration static class DispatcherConfig { + @Bean + public WebFilter contextFilter() { + return new ServerWebExchangeContextFilter(); + } + @Bean public DispatcherHandler webHandler() { return new DispatcherHandler(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java index 072cfddd2305..2b0380aea98c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/WebSocketIntegrationTests.java @@ -33,6 +33,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; +import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; import org.springframework.web.reactive.socket.client.WebSocketClient; @@ -216,8 +217,11 @@ private static class EchoWebSocketHandler implements WebSocketHandler { @Override public Mono handle(WebSocketSession session) { - // Use retain() for Reactor Netty - return session.send(session.receive().doOnNext(WebSocketMessage::retain)); + return Mono.deferContextual(contextView -> { + String key = ServerWebExchangeContextFilter.EXCHANGE_CONTEXT_ATTRIBUTE; + assertThat(contextView.getOrEmpty(key).orElse(null)).isNotNull(); + return session.send(session.receive().doOnNext(WebSocketMessage::retain)); + }); } } From ef6a582c78b1e5a11dae64cc9e9c9b9572646bc8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 15 Dec 2020 22:46:02 +0100 Subject: [PATCH 0137/1294] Clean up warnings in Gradle build --- .../config/ResourceHandlerRegistryTests.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java index 490cc21a1b6c..21b493829dd6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java @@ -33,7 +33,6 @@ import org.springframework.http.server.PathContainer; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; -import org.springframework.web.reactive.resource.AppCacheManifestTransformer; import org.springframework.web.reactive.resource.CachingResourceResolver; import org.springframework.web.reactive.resource.CachingResourceTransformer; import org.springframework.web.reactive.resource.CssLinkResourceTransformer; @@ -56,7 +55,7 @@ * * @author Rossen Stoyanchev */ -public class ResourceHandlerRegistryTests { +class ResourceHandlerRegistryTests { private ResourceHandlerRegistry registry; @@ -64,7 +63,7 @@ public class ResourceHandlerRegistryTests { @BeforeEach - public void setup() { + void setup() { this.registry = new ResourceHandlerRegistry(new GenericApplicationContext()); this.registration = this.registry.addResourceHandler("/resources/**"); this.registration.addResourceLocations("classpath:org/springframework/web/reactive/config/"); @@ -72,13 +71,13 @@ public void setup() { @Test - public void noResourceHandlers() throws Exception { + void noResourceHandlers() { this.registry = new ResourceHandlerRegistry(new GenericApplicationContext()); assertThat((Object) this.registry.getHandlerMapping()).isNull(); } @Test - public void mapPathToLocation() throws Exception { + void mapPathToLocation() { MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("")); exchange.getAttributes().put(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, PathContainer.parsePath("/testStylesheet.css")); @@ -93,7 +92,7 @@ public void mapPathToLocation() throws Exception { } @Test - public void cacheControl() { + void cacheControl() { assertThat(getHandler("/resources/**").getCacheControl()).isNull(); this.registration.setCacheControl(CacheControl.noCache().cachePrivate()); @@ -102,7 +101,7 @@ public void cacheControl() { } @Test - public void mediaTypes() { + void mediaTypes() { MediaType mediaType = MediaType.parseMediaType("foo/bar"); this.registration.setMediaTypes(Collections.singletonMap("bar", mediaType)); ResourceWebHandler requestHandler = this.registration.getRequestHandler(); @@ -112,7 +111,7 @@ public void mediaTypes() { } @Test - public void order() { + void order() { assertThat(this.registry.getHandlerMapping().getOrder()).isEqualTo(Integer.MAX_VALUE -1); this.registry.setOrder(0); @@ -120,13 +119,13 @@ public void order() { } @Test - public void hasMappingForPattern() { + void hasMappingForPattern() { assertThat(this.registry.hasMappingForPattern("/resources/**")).isTrue(); assertThat(this.registry.hasMappingForPattern("/whatever")).isFalse(); } @Test - public void resourceChain() throws Exception { + void resourceChain() { ResourceUrlProvider resourceUrlProvider = Mockito.mock(ResourceUrlProvider.class); this.registry.setResourceUrlProvider(resourceUrlProvider); ResourceResolver mockResolver = Mockito.mock(ResourceResolver.class); @@ -152,7 +151,7 @@ public void resourceChain() throws Exception { } @Test - public void resourceChainWithoutCaching() throws Exception { + void resourceChainWithoutCaching() { this.registration.resourceChain(false); ResourceWebHandler handler = getHandler("/resources/**"); @@ -166,13 +165,14 @@ public void resourceChainWithoutCaching() throws Exception { } @Test - public void resourceChainWithVersionResolver() throws Exception { + @SuppressWarnings("deprecation") + void resourceChainWithVersionResolver() { VersionResourceResolver versionResolver = new VersionResourceResolver() .addFixedVersionStrategy("fixed", "/**/*.js") .addContentVersionStrategy("/**"); this.registration.resourceChain(true).addResolver(versionResolver) - .addTransformer(new AppCacheManifestTransformer()); + .addTransformer(new org.springframework.web.reactive.resource.AppCacheManifestTransformer()); ResourceWebHandler handler = getHandler("/resources/**"); List resolvers = handler.getResourceResolvers(); @@ -186,17 +186,19 @@ public void resourceChainWithVersionResolver() throws Exception { assertThat(transformers).hasSize(3); assertThat(transformers.get(0)).isInstanceOf(CachingResourceTransformer.class); assertThat(transformers.get(1)).isInstanceOf(CssLinkResourceTransformer.class); - assertThat(transformers.get(2)).isInstanceOf(AppCacheManifestTransformer.class); + assertThat(transformers.get(2)).isInstanceOf(org.springframework.web.reactive.resource.AppCacheManifestTransformer.class); } @Test - public void resourceChainWithOverrides() throws Exception { + @SuppressWarnings("deprecation") + void resourceChainWithOverrides() { CachingResourceResolver cachingResolver = Mockito.mock(CachingResourceResolver.class); VersionResourceResolver versionResolver = Mockito.mock(VersionResourceResolver.class); WebJarsResourceResolver webjarsResolver = Mockito.mock(WebJarsResourceResolver.class); PathResourceResolver pathResourceResolver = new PathResourceResolver(); CachingResourceTransformer cachingTransformer = Mockito.mock(CachingResourceTransformer.class); - AppCacheManifestTransformer appCacheTransformer = Mockito.mock(AppCacheManifestTransformer.class); + org.springframework.web.reactive.resource.AppCacheManifestTransformer appCacheTransformer = + Mockito.mock(org.springframework.web.reactive.resource.AppCacheManifestTransformer.class); CssLinkResourceTransformer cssLinkTransformer = new CssLinkResourceTransformer(); this.registration.setCacheControl(CacheControl.maxAge(3600, TimeUnit.MILLISECONDS)) From 1292947f7837c2716d6e0335698af450d4f12732 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 15 Dec 2020 22:40:23 +0100 Subject: [PATCH 0138/1294] Introduce computeAttribute() in AttributeAccessor This commit introduces computeAttribute() as an interface default method in the AttributeAccessor API. This serves as a convenience analogous to the computeIfAbsent() method in java.util.Map. Closes gh-26281 --- .../core/AttributeAccessor.java | 46 +++++++++++++++++-- .../core/AttributeAccessorSupport.java | 15 +++++- .../core/AttributeAccessorSupportTests.java | 42 +++++++++++++---- .../context/support/DefaultTestContext.java | 14 +++++- 4 files changed, 100 insertions(+), 17 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java index bb81ccaaeaed..ea6d3a0c43f2 100644 --- a/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java @@ -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. @@ -16,20 +16,24 @@ package org.springframework.core; +import java.util.function.Function; + import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Interface defining a generic contract for attaching and accessing metadata * to/from arbitrary objects. * * @author Rob Harrop + * @author Sam Brannen * @since 2.0 */ public interface AttributeAccessor { /** * Set the attribute defined by {@code name} to the supplied {@code value}. - * If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}. + *

    If {@code value} is {@code null}, the attribute is {@link #removeAttribute removed}. *

    In general, users should take care to prevent overlaps with other * metadata attributes by using fully-qualified names, perhaps using * class or package names as prefix. @@ -40,16 +44,48 @@ public interface AttributeAccessor { /** * Get the value of the attribute identified by {@code name}. - * Return {@code null} if the attribute doesn't exist. + *

    Return {@code null} if the attribute doesn't exist. * @param name the unique attribute key * @return the current value of the attribute, if any */ @Nullable Object getAttribute(String name); + /** + * Compute a new value for the attribute identified by {@code name} if + * necessary and {@linkplain #setAttribute set} the new value in this + * {@code AttributeAccessor}. + *

    If a value for the attribute identified by {@code name} already exists + * in this {@code AttributeAccessor}, the existing value will be returned + * without applying the supplied compute function. + *

    The default implementation of this method is not thread safe but can + * overridden by concrete implementations of this interface. + * @param the type of the attribute value + * @param name the unique attribute key + * @param computeFunction a function that computes a new value for the attribute + * name; the function must not return a {@code null} value + * @return the existing value or newly computed value for the named attribute + * @see #getAttribute(String) + * @see #setAttribute(String, Object) + * @since 5.3.3 + */ + @SuppressWarnings("unchecked") + default T computeAttribute(String name, Function computeFunction) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(computeFunction, "Compute function must not be null"); + Object value = getAttribute(name); + if (value == null) { + value = computeFunction.apply(name); + Assert.state(value != null, + () -> String.format("Compute function must not return null for attribute named '%s'", name)); + setAttribute(name, value); + } + return (T) value; + } + /** * Remove the attribute identified by {@code name} and return its value. - * Return {@code null} if no attribute under {@code name} is found. + *

    Return {@code null} if no attribute under {@code name} is found. * @param name the unique attribute key * @return the last value of the attribute, if any */ @@ -58,7 +94,7 @@ public interface AttributeAccessor { /** * Return {@code true} if the attribute identified by {@code name} exists. - * Otherwise return {@code false}. + *

    Otherwise return {@code false}. * @param name the unique attribute key */ boolean hasAttribute(String name); diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java index df9c391b05aa..f5c788d5f408 100644 --- a/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java @@ -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. @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.LinkedHashMap; import java.util.Map; +import java.util.function.Function; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -32,6 +33,7 @@ * * @author Rob Harrop * @author Juergen Hoeller + * @author Sam Brannen * @since 2.0 */ @SuppressWarnings("serial") @@ -59,6 +61,17 @@ public Object getAttribute(String name) { return this.attributes.get(name); } + @Override + @SuppressWarnings("unchecked") + public T computeAttribute(String name, Function computeFunction) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(computeFunction, "Compute function must not be null"); + Object value = this.attributes.computeIfAbsent(name, computeFunction); + Assert.state(value != null, + () -> String.format("Compute function must not return null for attribute named '%s'", name)); + return (T) value; + } + @Override @Nullable public Object removeAttribute(String name) { diff --git a/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java b/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java index bd56c919cecc..1b0836193307 100644 --- a/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java +++ b/spring-core/src/test/java/org/springframework/core/AttributeAccessorSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -17,39 +17,61 @@ package org.springframework.core; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** + * Unit tests for {@link AttributeAccessorSupport}. + * * @author Rob Harrop * @author Sam Brannen * @since 2.0 */ class AttributeAccessorSupportTests { - private static final String NAME = "foo"; + private static final String NAME = "name"; + + private static final String VALUE = "value"; - private static final String VALUE = "bar"; + private final AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport(); - private AttributeAccessor attributeAccessor = new SimpleAttributeAccessorSupport(); @Test - void setAndGet() throws Exception { + void setAndGet() { this.attributeAccessor.setAttribute(NAME, VALUE); assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo(VALUE); } @Test - void setAndHas() throws Exception { + void setAndHas() { assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); this.attributeAccessor.setAttribute(NAME, VALUE); assertThat(this.attributeAccessor.hasAttribute(NAME)).isTrue(); } @Test - void remove() throws Exception { + void computeAttribute() { + AtomicInteger atomicInteger = new AtomicInteger(); + Function computeFunction = name -> "computed-" + atomicInteger.incrementAndGet(); + + assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); + this.attributeAccessor.computeAttribute(NAME, computeFunction); + assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-1"); + this.attributeAccessor.computeAttribute(NAME, computeFunction); + assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-1"); + + this.attributeAccessor.removeAttribute(NAME); + assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); + this.attributeAccessor.computeAttribute(NAME, computeFunction); + assertThat(this.attributeAccessor.getAttribute(NAME)).isEqualTo("computed-2"); + } + + @Test + void remove() { assertThat(this.attributeAccessor.hasAttribute(NAME)).isFalse(); this.attributeAccessor.setAttribute(NAME, VALUE); assertThat(this.attributeAccessor.removeAttribute(NAME)).isEqualTo(VALUE); @@ -57,13 +79,13 @@ void remove() throws Exception { } @Test - void attributeNames() throws Exception { + void attributeNames() { this.attributeAccessor.setAttribute(NAME, VALUE); this.attributeAccessor.setAttribute("abc", "123"); String[] attributeNames = this.attributeAccessor.attributeNames(); Arrays.sort(attributeNames); - assertThat(Arrays.binarySearch(attributeNames, NAME) > -1).isTrue(); - assertThat(Arrays.binarySearch(attributeNames, "abc") > -1).isTrue(); + assertThat(Arrays.binarySearch(attributeNames, "abc")).isEqualTo(0); + assertThat(Arrays.binarySearch(attributeNames, NAME)).isEqualTo(1); } @SuppressWarnings("serial") diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java index 5cc2dbf91217..0d0d0af7e139 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; @@ -200,6 +201,17 @@ public Object getAttribute(String name) { return this.attributes.get(name); } + @Override + @SuppressWarnings("unchecked") + public T computeAttribute(String name, Function computeFunction) { + Assert.notNull(name, "Name must not be null"); + Assert.notNull(computeFunction, "Compute function must not be null"); + Object value = this.attributes.computeIfAbsent(name, computeFunction); + Assert.state(value != null, + () -> String.format("Compute function must not return null for attribute named '%s'", name)); + return (T) value; + } + @Override @Nullable public Object removeAttribute(String name) { From b3a47c76f8776d9abfdc70aa83c84c252da4426d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 8 Dec 2020 15:07:58 +0100 Subject: [PATCH 0139/1294] Introduce TestContextAnnotationUtils.hasAnnotation() --- .../test/context/BootstrapUtils.java | 2 +- .../test/context/TestContextAnnotationUtils.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java index 1469160cc1cb..ff88ade86241 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java @@ -179,7 +179,7 @@ private static Class resolveExplicitTestContextBootstrapper(Class testClas } private static Class resolveDefaultTestContextBootstrapper(Class testClass) throws Exception { - boolean webApp = (TestContextAnnotationUtils.findMergedAnnotation(testClass, webAppConfigurationClass) != null); + boolean webApp = TestContextAnnotationUtils.hasAnnotation(testClass, webAppConfigurationClass); String bootstrapperClassName = (webApp ? DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME : DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME); return ClassUtils.forName(bootstrapperClassName, BootstrapUtils.class.getClassLoader()); diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java index 1c01b0a07ea8..4d8550e6f29d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java @@ -78,6 +78,22 @@ public abstract class TestContextAnnotationUtils { private static volatile EnclosingConfiguration defaultEnclosingConfigurationMode; + /** + * Determine if an annotation of the specified {@code annotationType} is + * present or meta-present on the supplied {@link Class} according to the + * search algorithm used in {@link #findMergedAnnotation(Class, Class)}. + *

    If this method returns {@code true}, then {@code findMergedAnnotation(...)} + * will return a non-null value. + * @param clazz the class to look for annotations on + * @param annotationType the type of annotation to look for + * @return {@code true} if a matching annotation is present + * @since 5.3.3 + * @see #findMergedAnnotation(Class, Class) + */ + public static boolean hasAnnotation(Class clazz, Class annotationType) { + return (findMergedAnnotation(clazz, annotationType) != null); + } + /** * Find the first annotation of the specified {@code annotationType} within * the annotation hierarchy above the supplied class, merge that From fbd2ffdd2343174bb119c2c996f8755957bad8c8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 16 Dec 2020 22:27:33 +0100 Subject: [PATCH 0140/1294] Consistent declarations and assertions in MockMultipartFile See gh-26261 --- .../springframework/mock/web/MockMultipartFile.java | 10 ++++++---- .../web/testfixture/servlet/MockMultipartFile.java | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java index 359d945a1d3e..781ab7a6e48f 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java @@ -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. @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStream; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; @@ -42,10 +43,10 @@ public class MockMultipartFile implements MultipartFile { private final String name; - private String originalFilename; + private final String originalFilename; @Nullable - private String contentType; + private final String contentType; private final byte[] content; @@ -79,7 +80,7 @@ public MockMultipartFile(String name, InputStream contentStream) throws IOExcept public MockMultipartFile( String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content) { - Assert.hasLength(name, "Name must not be null"); + Assert.hasLength(name, "Name must not be empty"); this.name = name; this.originalFilename = (originalFilename != null ? originalFilename : ""); this.contentType = contentType; @@ -108,6 +109,7 @@ public String getName() { } @Override + @NonNull public String getOriginalFilename() { return this.originalFilename; } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockMultipartFile.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockMultipartFile.java index 9250fb076c11..849731ac5a68 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockMultipartFile.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockMultipartFile.java @@ -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. @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.InputStream; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; @@ -42,10 +43,10 @@ public class MockMultipartFile implements MultipartFile { private final String name; - private String originalFilename; + private final String originalFilename; @Nullable - private String contentType; + private final String contentType; private final byte[] content; @@ -79,7 +80,7 @@ public MockMultipartFile(String name, InputStream contentStream) throws IOExcept public MockMultipartFile( String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content) { - Assert.hasLength(name, "Name must not be null"); + Assert.hasLength(name, "Name must not be empty"); this.name = name; this.originalFilename = (originalFilename != null ? originalFilename : ""); this.contentType = contentType; @@ -108,6 +109,7 @@ public String getName() { } @Override + @NonNull public String getOriginalFilename() { return this.originalFilename; } From 00b56c026ac2ce955c6107099f9cf2daf63d37e0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 16 Dec 2020 22:27:41 +0100 Subject: [PATCH 0141/1294] Consistent handling of NullBean instances in resolveNamedBean Closes gh-26271 --- .../factory/support/AbstractBeanFactory.java | 19 ++++++++++------- .../support/DefaultListableBeanFactory.java | 21 +++++++++++++++---- ...notationConfigApplicationContextTests.java | 6 ++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 443b7ee586dd..562c00fd55f1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -250,7 +250,7 @@ protected T doGetBean( throws BeansException { String beanName = transformedBeanName(name); - Object bean; + Object beanInstance; // Eagerly check singleton cache for manually registered singletons. Object sharedInstance = getSingleton(beanName); @@ -264,7 +264,7 @@ protected T doGetBean( logger.trace("Returning cached instance of singleton bean '" + beanName + "'"); } } - bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { @@ -342,7 +342,7 @@ else if (requiredType != null) { throw ex; } }); - bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } else if (mbd.isPrototype()) { @@ -355,7 +355,7 @@ else if (mbd.isPrototype()) { finally { afterPrototypeCreation(beanName); } - bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); } else { @@ -377,7 +377,7 @@ else if (mbd.isPrototype()) { afterPrototypeCreation(beanName); } }); - bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); } catch (IllegalStateException ex) { throw new ScopeNotActiveException(beanName, scopeName, ex); @@ -395,14 +395,19 @@ else if (mbd.isPrototype()) { } } + return adaptBeanInstance(name, beanInstance, requiredType); + } + + @SuppressWarnings("unchecked") + T adaptBeanInstance(String name, Object bean, @Nullable Class requiredType) { // Check if required type matches the type of the actual bean instance. if (requiredType != null && !requiredType.isInstance(bean)) { try { - T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); + Object convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); if (convertedBean == null) { throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); } - return convertedBean; + return (T) convertedBean; } catch (TypeMismatchException ex) { if (logger.isTraceEnabled()) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index dc79d3c6ad75..048612fbed73 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1231,8 +1231,7 @@ private NamedBeanHolder resolveNamedBean( } if (candidateNames.length == 1) { - String beanName = candidateNames[0]; - return new NamedBeanHolder<>(beanName, (T) getBean(beanName, requiredType.toClass(), args)); + return resolveNamedBean(candidateNames[0], requiredType, args); } else if (candidateNames.length > 1) { Map candidates = CollectionUtils.newLinkedHashMap(candidateNames.length); @@ -1251,8 +1250,11 @@ else if (candidateNames.length > 1) { } if (candidateName != null) { Object beanInstance = candidates.get(candidateName); - if (beanInstance == null || beanInstance instanceof Class) { - beanInstance = getBean(candidateName, requiredType.toClass(), args); + if (beanInstance == null) { + return null; + } + if (beanInstance instanceof Class) { + return resolveNamedBean(candidateName, requiredType, args); } return new NamedBeanHolder<>(candidateName, (T) beanInstance); } @@ -1264,6 +1266,17 @@ else if (candidateNames.length > 1) { return null; } + @Nullable + private NamedBeanHolder resolveNamedBean( + String beanName, ResolvableType requiredType, @Nullable Object[] args) throws BeansException { + + Object bean = getBean(beanName, null, args); + if (bean instanceof NullBean) { + return null; + } + return new NamedBeanHolder(beanName, adaptBeanInstance(beanName, bean, requiredType.toClass())); + } + @Override @Nullable public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java index f2a7c86b4093..336432ff0c77 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java @@ -271,9 +271,15 @@ void individualBeanWithNullReturningSupplier() { assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanA.class), "a")).isTrue(); assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanB.class), "b")).isTrue(); assertThat(ObjectUtils.containsElement(context.getBeanNamesForType(BeanC.class), "c")).isTrue(); + assertThat(context.getBeansOfType(BeanA.class)).isEmpty(); assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()).isSameAs(context.getBean(BeanB.class)); assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()).isSameAs(context.getBean(BeanC.class)); + + assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> + context.getBeanFactory().resolveNamedBean(BeanA.class)); + assertThat(context.getBeanFactory().resolveNamedBean(BeanB.class).getBeanInstance()).isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBeanFactory().resolveNamedBean(BeanC.class).getBeanInstance()).isSameAs(context.getBean(BeanC.class)); } @Test From a109b4c31a29a9a6c147e3238e14be27d70527f9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 16 Dec 2020 22:27:57 +0100 Subject: [PATCH 0142/1294] Translate PostgreSQL code 21000 (cardinality_violation) Closes gh-26276 --- .../org/springframework/jdbc/support/sql-error-codes.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml b/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml index f0ed6570be37..d2e587967c16 100644 --- a/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml +++ b/spring-jdbc/src/main/resources/org/springframework/jdbc/support/sql-error-codes.xml @@ -247,7 +247,7 @@ 03000,42000,42601,42602,42622,42804,42P01 - 23505 + 21000,23505 23000,23502,23503,23514 From d3e1c54354ada0fb8955410469bd3bc20e27d76a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 17 Dec 2020 10:46:13 +0100 Subject: [PATCH 0143/1294] Upgrade to Hibernate Validator 6.1.7, Jetty Reactive HttpClient 1.1.5, Joda-Time 2.10.8, XMLUnit 2.8.1 --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 22d4def6ebba..49a237ccb573 100644 --- a/build.gradle +++ b/build.gradle @@ -124,7 +124,7 @@ configure(allprojects) { project -> dependency "org.ehcache:jcache:1.0.1" dependency "org.ehcache:ehcache:3.4.0" dependency "org.hibernate:hibernate-core:5.4.25.Final" - dependency "org.hibernate:hibernate-validator:6.1.6.Final" + dependency "org.hibernate:hibernate-validator:6.1.7.Final" dependency "org.webjars:webjars-locator-core:0.46" dependency "org.webjars:underscorejs:1.8.3" @@ -162,7 +162,7 @@ configure(allprojects) { project -> } dependency 'org.apache.httpcomponents.client5:httpclient5:5.0.3' dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.3' - dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.4" + dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.5" dependency "org.jruby:jruby:9.2.13.0" dependency "org.python:jython-standalone:2.7.1" @@ -191,7 +191,7 @@ configure(allprojects) { project -> dependency "org.hamcrest:hamcrest:2.1" dependency "org.awaitility:awaitility:3.1.6" dependency "org.assertj:assertj-core:3.18.1" - dependencySet(group: 'org.xmlunit', version: '2.6.2') { + dependencySet(group: 'org.xmlunit', version: '2.8.1') { entry 'xmlunit-assertj' entry('xmlunit-matchers') { exclude group: "org.hamcrest", name: "hamcrest-core" @@ -236,7 +236,7 @@ configure(allprojects) { project -> dependency "com.ibm.websphere:uow:6.0.2.17" dependency "com.jamonapi:jamon:2.82" - dependency "joda-time:joda-time:2.10.6" + dependency "joda-time:joda-time:2.10.8" dependency "org.eclipse.persistence:org.eclipse.persistence.jpa:2.7.7" dependency "org.javamoney:moneta:1.3" From f07fc76cf30cbfb6ae71134b653f26af9649ea1b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 17 Dec 2020 14:49:57 +0000 Subject: [PATCH 0144/1294] Limit scheme/host check in fromUriString to HTTP URLs Closes gh-26258 --- .../org/springframework/web/util/UriComponentsBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java index e22a8d7c2fd9..5ec7e8eedf17 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java @@ -252,8 +252,8 @@ public static UriComponentsBuilder fromUriString(String uri) { builder.schemeSpecificPart(ssp); } else { - if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) { - throw new IllegalArgumentException("[" + uri + "] is not a valid URI"); + if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) { + throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL"); } builder.userInfo(userInfo); builder.host(host); From 0cf5005a3d8bfe387ccc970c95953351fc83fbe5 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 17 Dec 2020 17:18:38 +0000 Subject: [PATCH 0145/1294] Apply abortOnCancel in JettyClientHttpConnector This new option allows a cancel signal to abort the request, which is how we expect a connection to be aborted in a reactive chain that involves the WebClient. Closes gh-26287 --- .../http/client/reactive/JettyClientHttpConnector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java index 15fcedb81335..01a7ccd36b42 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java @@ -123,7 +123,7 @@ public Mono connect(HttpMethod method, URI uri, Request request = this.httpClient.newRequest(uri).method(method.toString()); return requestCallback.apply(new JettyClientHttpRequest(request, this.bufferFactory)) - .then(Mono.fromDirect(ReactiveRequest.newBuilder(request).build() + .then(Mono.fromDirect(ReactiveRequest.newBuilder(request).abortOnCancel(true).build() .response((reactiveResponse, chunkPublisher) -> { Flux content = Flux.from(chunkPublisher).map(this::toDataBuffer); return Mono.just(new JettyClientHttpResponse(reactiveResponse, content)); From 499be70a717b8d20c544bc2eac4fe5dacedc7f28 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 17 Dec 2020 17:31:44 +0000 Subject: [PATCH 0146/1294] Update async dispatch check in OncePerRequestFilter We no longer need to rely on an indirect check since Servlet 3.0 is expected so we can just check the DispatcherType of the request. Closes gh-26282 --- .../springframework/web/filter/OncePerRequestFilter.java | 4 ++-- .../web/filter/CharacterEncodingFilterTests.java | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java b/spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java index a775b8b504e4..7aec81b07c29 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -145,7 +145,7 @@ private boolean skipDispatch(HttpServletRequest request) { * @see WebAsyncManager#hasConcurrentResult() */ protected boolean isAsyncDispatch(HttpServletRequest request) { - return WebAsyncUtils.getAsyncManager(request).hasConcurrentResult(); + return request.getDispatcherType().equals(DispatcherType.ASYNC); } /** diff --git a/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java index 19bd055b006b..8ab956d66f43 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/CharacterEncodingFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -16,6 +16,7 @@ package org.springframework.web.filter; +import javax.servlet.DispatcherType; import javax.servlet.FilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -51,6 +52,7 @@ public void forceEncodingAlwaysSetsEncoding() throws Exception { request.setCharacterEncoding(ENCODING); given(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).willReturn(null); given(request.getAttribute(filteredName(FILTER_NAME))).willReturn(null); + given(request.getDispatcherType()).willReturn(DispatcherType.REQUEST); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain filterChain = mock(FilterChain.class); @@ -71,6 +73,7 @@ public void encodingIfEmptyAndNotForced() throws Exception { given(request.getCharacterEncoding()).willReturn(null); given(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).willReturn(null); given(request.getAttribute(filteredName(FILTER_NAME))).willReturn(null); + given(request.getDispatcherType()).willReturn(DispatcherType.REQUEST); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -92,6 +95,7 @@ public void doesNotIfEncodingIsNotEmptyAndNotForced() throws Exception { given(request.getCharacterEncoding()).willReturn(ENCODING); given(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).willReturn(null); given(request.getAttribute(filteredName(FILTER_NAME))).willReturn(null); + given(request.getDispatcherType()).willReturn(DispatcherType.REQUEST); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -112,6 +116,7 @@ public void withBeanInitialization() throws Exception { given(request.getCharacterEncoding()).willReturn(null); given(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).willReturn(null); given(request.getAttribute(filteredName(FILTER_NAME))).willReturn(null); + given(request.getDispatcherType()).willReturn(DispatcherType.REQUEST); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -135,6 +140,7 @@ public void withIncompleteInitialization() throws Exception { given(request.getCharacterEncoding()).willReturn(null); given(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).willReturn(null); given(request.getAttribute(filteredName(CharacterEncodingFilter.class.getName()))).willReturn(null); + given(request.getDispatcherType()).willReturn(DispatcherType.REQUEST); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -156,6 +162,7 @@ public void setForceEncodingOnRequestOnly() throws Exception { request.setCharacterEncoding(ENCODING); given(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE)).willReturn(null); given(request.getAttribute(filteredName(FILTER_NAME))).willReturn(null); + given(request.getDispatcherType()).willReturn(DispatcherType.REQUEST); HttpServletResponse response = mock(HttpServletResponse.class); FilterChain filterChain = mock(FilterChain.class); From 947255e3774fe6248c59d2cdd6a1b06b9f6b5d9b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 18 Dec 2020 11:55:20 +0100 Subject: [PATCH 0147/1294] Always propagate checked exceptions from Kotlin code behind CGLIB proxies Closes gh-23844 --- .../springframework/aop/framework/CglibAopProxy.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index 69fe9a048773..e2b822816dbf 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -48,6 +48,7 @@ import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.NoOp; +import org.springframework.core.KotlinDetector; import org.springframework.core.SmartClassLoader; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -752,10 +753,17 @@ public Object proceed() throws Throwable { throw ex; } catch (Exception ex) { - if (ReflectionUtils.declaresException(getMethod(), ex.getClass())) { + if (ReflectionUtils.declaresException(getMethod(), ex.getClass()) || + KotlinDetector.isKotlinType(getMethod().getDeclaringClass())) { + // Propagate original exception if declared on the target method + // (with callers expecting it). Always propagate it for Kotlin code + // since checked exceptions do not have to be explicitly declared there. throw ex; } else { + // Checked exception thrown in the interceptor but not declared on the + // target method signature -> apply an UndeclaredThrowableException, + // aligned with standard JDK dynamic proxy behavior. throw new UndeclaredThrowableException(ex); } } From 65a395ef0e96c5e5ce28526d1fe975daaa566b0d Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sat, 19 Dec 2020 15:29:40 +0100 Subject: [PATCH 0148/1294] Fix syntax in Kotlin example --- src/docs/asciidoc/testing.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/testing.adoc b/src/docs/asciidoc/testing.adoc index f4dc2f87e14a..09b2b3936ff9 100644 --- a/src/docs/asciidoc/testing.adoc +++ b/src/docs/asciidoc/testing.adoc @@ -8183,8 +8183,8 @@ assertions use the https://joel-costigliola.github.io/assertj/[AssertJ] assertio [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] .Kotlin ---- - assertThat(viewMessagePage.message.isEqualTo(expectedMessage) - assertThat(viewMessagePage.success.isEqualTo("Successfully created a new message") + assertThat(viewMessagePage.message).isEqualTo(expectedMessage) + assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message") ---- We can see that our `ViewMessagePage` lets us interact with our custom domain model. For From 1565f4b83e7c48eeec9dc74f7eb042dce4dbb49a Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 19 Aug 2020 22:16:34 +0200 Subject: [PATCH 0149/1294] Introduce ApplicationEvents to assert events published during tests This commit introduces a new feature in the Spring TestContext Framework (TCF) that provides support for recording application events published in the ApplicationContext so that assertions can be performed against those events within tests. All events published during the execution of a single test are made available via the ApplicationEvents API which allows one to process the events as a java.util.Stream. The following example demonstrates usage of this new feature. @SpringJUnitConfig(/* ... */) @RecordApplicationEvents class OrderServiceTests { @Autowired OrderService orderService; @Autowired ApplicationEvents events; @Test void submitOrder() { // Invoke method in OrderService that publishes an event orderService.submitOrder(new Order(/* ... */)); // Verify that an OrderSubmitted event was published int numEvents = events.stream(OrderSubmitted.class).count(); assertThat(numEvents).isEqualTo(1); } } To enable the feature, a test class must be annotated or meta-annotated with @RecordApplicationEvents. Behind the scenes, a new ApplicationEventsTestExecutionListener manages the registration of ApplicationEvents for the current thread at various points within the test execution lifecycle and makes the current instance of ApplicationEvents available to tests via an @Autowired field in the test class. The latter is made possible by a custom ObjectFactory that is registered as a "resolvable dependency". Thanks to the magic of ObjectFactoryDelegatingInvocationHandler in the spring-beans module, the ApplicationEvents instance injected into test classes is effectively a scoped-proxy that always accesses the ApplicationEvents managed for the current test. The current ApplicationEvents instance is stored in a ThreadLocal variable which is made available in the ApplicationEventsHolder. Although this class is public, it is only intended for use within the TCF or in the implementation of third-party extensions. ApplicationEventsApplicationListener is responsible for listening to all application events and recording them in the current ApplicationEvents instance. A single ApplicationEventsApplicationListener is registered with the test's ApplicationContext by the ApplicationEventsTestExecutionListener. The SpringExtension has also been updated to support parameters of type ApplicationEvents via the JUnit Jupiter ParameterResolver extension API. This allows JUnit Jupiter based tests to receive access to the current ApplicationEvents via test and lifecycle method parameters as an alternative to @Autowired fields in the test class. Closes gh-25616 --- .../test/context/TestExecutionListener.java | 4 +- .../test/context/TestExecutionListeners.java | 1 + .../test/context/event/ApplicationEvents.java | 82 ++++++ .../ApplicationEventsApplicationListener.java | 41 +++ .../event/ApplicationEventsHolder.java | 107 +++++++ ...pplicationEventsTestExecutionListener.java | 138 +++++++++ .../event/DefaultApplicationEvents.java | 65 +++++ .../event/RecordApplicationEvents.java | 49 ++++ .../junit/jupiter/SpringExtension.java | 12 + .../AbstractJUnit4SpringContextTests.java | 9 +- ...TransactionalJUnit4SpringContextTests.java | 8 +- .../AbstractTestNGSpringContextTests.java | 9 +- ...TransactionalTestNGSpringContextTests.java | 8 +- .../main/resources/META-INF/spring.factories | 1 + .../context/TestExecutionListenersTests.java | 60 ++-- .../jupiter/event/DefaultPublishedEvents.java | 91 ++++++ ...iterApplicationEventsIntegrationTests.java | 275 ++++++++++++++++++ ...llelApplicationEventsIntegrationTests.java | 173 +++++++++++ .../junit/jupiter/event/PublishedEvents.java | 87 ++++++ .../event/PublishedEventsExtension.java | 39 +++ .../PublishedEventsIntegrationTests.java | 60 ++++ ...nit4ApplicationEventsIntegrationTests.java | 123 ++++++++ ...stNGApplicationEventsIntegrationTests.java | 160 ++++++++++ src/docs/asciidoc/testing.adoc | 112 ++++++- 24 files changed, 1679 insertions(+), 35 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/event/ApplicationEvents.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsApplicationListener.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsTestExecutionListener.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/event/DefaultApplicationEvents.java create mode 100644 spring-test/src/main/java/org/springframework/test/context/event/RecordApplicationEvents.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/DefaultPublishedEvents.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/JUnitJupiterApplicationEventsIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/ParallelApplicationEventsIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEvents.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEventsExtension.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit/jupiter/event/PublishedEventsIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/junit4/event/JUnit4ApplicationEventsIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/testng/event/TestNGApplicationEventsIntegrationTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java index 815f1940d39c..a9b188e6e92d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -48,6 +48,8 @@ * ServletTestExecutionListener} *

  • {@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener * DirtiesContextBeforeModesTestExecutionListener}
  • + *
  • {@link org.springframework.test.context.event.ApplicationEventsTestExecutionListener + * ApplicationEventsTestExecutionListener}
  • *
  • {@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener * DependencyInjectionTestExecutionListener}
  • *
  • {@link org.springframework.test.context.support.DirtiesContextTestExecutionListener diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java index b1b38149d1dd..444a372f7f7c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java @@ -67,6 +67,7 @@ * {@link #value}, but it may be used instead of {@link #value}. * @see org.springframework.test.context.web.ServletTestExecutionListener * @see org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener + * @see org.springframework.test.context.event.ApplicationEventsTestExecutionListener * @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener * @see org.springframework.test.context.support.DirtiesContextTestExecutionListener * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEvents.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEvents.java new file mode 100644 index 000000000000..7009fe2376f1 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEvents.java @@ -0,0 +1,82 @@ +/* + * 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.test.context.event; + +import java.util.stream.Stream; + +import org.springframework.context.ApplicationEvent; + +/** + * {@code ApplicationEvents} encapsulates all {@linkplain ApplicationEvent + * application events} that were fired during the execution of a single test method. + * + *

    To use {@code ApplicationEvents} in your tests, do the following. + *

      + *
    • Ensure that your test class is annotated or meta-annotated with + * {@link RecordApplicationEvents @RecordApplicationEvents}.
    • + *
    • Ensure that the {@link ApplicationEventsTestExecutionListener} is + * registered. Note, however, that it is registered by default and only needs + * to be manually registered if you have custom configuration via + * {@link org.springframework.test.context.TestExecutionListeners @TestExecutionListeners} + * that does not include the default listeners.
    • + *
    • Annotate a field of type {@code ApplicationEvents} with + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} and + * use that instance of {@code ApplicationEvents} in your test and lifecycle methods.
    • + *
    • With JUnit Jupiter, you may optionally declare a parameter of type + * {@code ApplicationEvents} in a test or lifecycle method as an alternative to + * an {@code @Autowired} field in the test class.
    • + *
    + * + * @author Sam Brannen + * @author Oliver Drotbohm + * @since 5.3.3 + * @see RecordApplicationEvents + * @see ApplicationEventsTestExecutionListener + * @see org.springframework.context.ApplicationEvent + */ +public interface ApplicationEvents { + + /** + * Stream all application events that were fired during test execution. + * @return a stream of all application events + * @see #stream(Class) + * @see #clear() + */ + Stream stream(); + + /** + * Stream all application events or event payloads of the given type that + * were fired during test execution. + * @param the event type + * @param type the type of events or payloads to stream; never {@code null} + * @return a stream of all application events or event payloads of the + * specified type + * @see #stream() + * @see #clear() + */ + Stream stream(Class type); + + /** + * Clear all application events recorded by this {@code ApplicationEvents} instance. + *

    Subsequent calls to {@link #stream()} or {@link #stream(Class)} will + * only include events recorded since this method was invoked. + * @see #stream() + * @see #stream(Class) + */ + void clear(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsApplicationListener.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsApplicationListener.java new file mode 100644 index 000000000000..a72d80238699 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsApplicationListener.java @@ -0,0 +1,41 @@ +/* + * 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.test.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * {@link ApplicationListener} that listens to all events and adds them to the + * current {@link ApplicationEvents} instance if registered for the current thread. + * + * @author Sam Brannen + * @author Oliver Drotbohm + * @since 5.3.3 + */ +class ApplicationEventsApplicationListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationEvent event) { + DefaultApplicationEvents applicationEvents = + (DefaultApplicationEvents) ApplicationEventsHolder.getApplicationEvents(); + if (applicationEvents != null) { + applicationEvents.addEvent(event); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java new file mode 100644 index 000000000000..aaa8dce30238 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java @@ -0,0 +1,107 @@ +/* + * 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.test.context.event; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Holder class to expose the application events published during the execution + * of a test in the form of a thread-bound {@link ApplicationEvents} object. + * + *

    {@code ApplicationEvents} are registered in this holder and managed by + * the {@link ApplicationEventsTestExecutionListener}. + * + *

    Although this class is {@code public}, it is only intended for use within + * the Spring TestContext Framework or in the implementation of + * third-party extensions. Test authors should therefore allow the current + * instance of {@code ApplicationEvents} to be + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} + * into a field in the test class or injected via a parameter in test and + * lifecycle methods when using JUnit Jupiter and the {@link + * org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension}. + * + * @author Sam Brannen + * @author Oliver Drotbohm + * @since 5.3.3 + * @see ApplicationEvents + * @see RecordApplicationEvents + * @see ApplicationEventsTestExecutionListener + */ +public abstract class ApplicationEventsHolder { + + private static final ThreadLocal applicationEvents = new ThreadLocal<>(); + + + private ApplicationEventsHolder() { + // no-op to prevent instantiation of this holder class + } + + + /** + * Get the {@link ApplicationEvents} for the current thread. + * @return the current {@code ApplicationEvents}, or {@code null} if not registered + */ + @Nullable + public static ApplicationEvents getApplicationEvents() { + return applicationEvents.get(); + } + + /** + * Get the {@link ApplicationEvents} for the current thread. + * @return the current {@code ApplicationEvents} + * @throws IllegalStateException if an instance of {@code ApplicationEvents} + * has not been registered for the current thread + */ + @Nullable + public static ApplicationEvents getRequiredApplicationEvents() { + ApplicationEvents events = applicationEvents.get(); + Assert.state(events != null, "Failed to retrieve ApplicationEvents for the current thread. " + + "Ensure that your test class is annotated with @RecordApplicationEvents " + + "and that the ApplicationEventsTestExecutionListener is registered."); + return events; + } + + + /** + * Register a new {@link DefaultApplicationEvents} instance to be used for the + * current thread, if necessary. + *

    If {@link #registerApplicationEvents()} has already been called for the + * current thread, this method does not do anything. + */ + static void registerApplicationEventsIfNecessary() { + if (getApplicationEvents() == null) { + registerApplicationEvents(); + } + } + + /** + * Register a new {@link DefaultApplicationEvents} instance to be used for the + * current thread. + */ + static void registerApplicationEvents() { + applicationEvents.set(new DefaultApplicationEvents()); + } + + /** + * Remove the registration of the {@link ApplicationEvents} for the current thread. + */ + static void unregisterApplicationEvents() { + applicationEvents.remove(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsTestExecutionListener.java new file mode 100644 index 000000000000..c2362297e0a6 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsTestExecutionListener.java @@ -0,0 +1,138 @@ +/* + * 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.test.context.event; + +import java.io.Serializable; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.Conventions; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.util.Assert; + +/** + * {@code TestExecutionListener} which provides support for {@link ApplicationEvents}. + * + *

    This listener manages the registration of {@code ApplicationEvents} for the + * current thread at various points within the test execution lifecycle and makes + * the current instance of {@code ApplicationEvents} available to tests via an + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} + * field in the test class. + * + *

    If the test class is not annotated or meta-annotated with + * {@link RecordApplicationEvents @RecordApplicationEvents}, this listener + * effectively does nothing. + * + * @author Sam Brannen + * @since 5.3.3 + * @see ApplicationEvents + * @see ApplicationEventsHolder + */ +public class ApplicationEventsTestExecutionListener extends AbstractTestExecutionListener { + + /** + * Attribute name for a {@link TestContext} attribute which indicates + * whether the test class for the given test context is annotated with + * {@link RecordApplicationEvents @RecordApplicationEvents}. + *

    Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. + */ + private static final String RECORD_APPLICATION_EVENTS = Conventions.getQualifiedAttributeName( + ApplicationEventsTestExecutionListener.class, "recordApplicationEvents"); + + private static final Object applicationEventsMonitor = new Object(); + + + /** + * Returns {@code 1800}. + */ + @Override + public final int getOrder() { + return 1800; + } + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + if (recordApplicationEvents(testContext)) { + registerListenerAndResolvableDependencyIfNecessary(testContext.getApplicationContext()); + ApplicationEventsHolder.registerApplicationEvents(); + } + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + if (recordApplicationEvents(testContext)) { + // Register a new ApplicationEvents instance for the current thread + // in case the test instance is shared -- for example, in TestNG or + // JUnit Jupiter with @TestInstance(PER_CLASS) semantics. + ApplicationEventsHolder.registerApplicationEventsIfNecessary(); + } + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + if (recordApplicationEvents(testContext)) { + ApplicationEventsHolder.unregisterApplicationEvents(); + } + } + + private boolean recordApplicationEvents(TestContext testContext) { + return testContext.computeAttribute(RECORD_APPLICATION_EVENTS, name -> + TestContextAnnotationUtils.hasAnnotation(testContext.getTestClass(), RecordApplicationEvents.class)); + } + + private void registerListenerAndResolvableDependencyIfNecessary(ApplicationContext applicationContext) { + Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext, + "The ApplicationContext for the test must be an AbstractApplicationContext"); + AbstractApplicationContext aac = (AbstractApplicationContext) applicationContext; + // Synchronize to avoid race condition in parallel test execution + synchronized(applicationEventsMonitor) { + boolean notAlreadyRegistered = aac.getApplicationListeners().stream() + .map(Object::getClass) + .noneMatch(ApplicationEventsApplicationListener.class::equals); + if (notAlreadyRegistered) { + // Register a new ApplicationEventsApplicationListener. + aac.addApplicationListener(new ApplicationEventsApplicationListener()); + + // Register ApplicationEvents as a resolvable dependency for @Autowired support in test classes. + ConfigurableListableBeanFactory beanFactory = aac.getBeanFactory(); + beanFactory.registerResolvableDependency(ApplicationEvents.class, new ApplicationEventsObjectFactory()); + } + } + } + + /** + * Factory that exposes the current {@link ApplicationEvents} object on demand. + */ + @SuppressWarnings("serial") + private static class ApplicationEventsObjectFactory implements ObjectFactory, Serializable { + + @Override + public ApplicationEvents getObject() { + return ApplicationEventsHolder.getRequiredApplicationEvents(); + } + + @Override + public String toString() { + return "Current ApplicationEvents"; + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/DefaultApplicationEvents.java b/spring-test/src/main/java/org/springframework/test/context/event/DefaultApplicationEvents.java new file mode 100644 index 000000000000..22d2eff61bfb --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/DefaultApplicationEvents.java @@ -0,0 +1,65 @@ +/* + * 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.test.context.event; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.PayloadApplicationEvent; + +/** + * Default implementation of {@link ApplicationEvents}. + * + * @author Oliver Drotbohm + * @author Sam Brannen + * @since 5.3.3 + */ +class DefaultApplicationEvents implements ApplicationEvents { + + private final List events = new ArrayList<>(); + + + void addEvent(ApplicationEvent event) { + this.events.add(event); + } + + @Override + public Stream stream() { + return this.events.stream(); + } + + @Override + public Stream stream(Class type) { + return this.events.stream() + .map(this::unwrapPayloadEvent) + .filter(type::isInstance) + .map(type::cast); + } + + @Override + public void clear() { + this.events.clear(); + } + + private Object unwrapPayloadEvent(Object source) { + return (PayloadApplicationEvent.class.isInstance(source) ? + ((PayloadApplicationEvent) source).getPayload() : source); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/event/RecordApplicationEvents.java b/spring-test/src/main/java/org/springframework/test/context/event/RecordApplicationEvents.java new file mode 100644 index 000000000000..4a1e8e050059 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/event/RecordApplicationEvents.java @@ -0,0 +1,49 @@ +/* + * 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.test.context.event; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @RecordApplicationEvents} is a class-level annotation that is used to + * instruct the Spring TestContext Framework to record all + * {@linkplain org.springframework.context.ApplicationEvent application events} + * that are published in the {@link org.springframework.context.ApplicationContext + * ApplicationContext} during the execution of a single test. + * + *

    The recorded events can be accessed via the {@link ApplicationEvents} API + * within your tests. + * + *

    This annotation may be used as a meta-annotation to create custom + * composed annotations. + * + * @author Sam Brannen + * @since 5.3.3 + * @see ApplicationEvents + * @see ApplicationEventsTestExecutionListener + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface RecordApplicationEvents { +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java index cf72a2b24138..83030f37b22c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java @@ -52,6 +52,7 @@ import org.springframework.lang.Nullable; import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.event.ApplicationEvents; import org.springframework.test.context.support.PropertyProvider; import org.springframework.test.context.support.TestConstructorUtils; import org.springframework.util.Assert; @@ -218,6 +219,7 @@ public void afterEach(ExtensionContext context) throws Exception { * invoked with a fallback {@link PropertyProvider} that delegates its lookup * to {@link ExtensionContext#getConfigurationParameter(String)}.

  • *
  • The parameter is of type {@link ApplicationContext} or a sub-type thereof.
  • + *
  • The parameter is of type {@link ApplicationEvents} or a sub-type thereof.
  • *
  • {@link ParameterResolutionDelegate#isAutowirable} returns {@code true}.
  • * *

    WARNING: If a test class {@code Constructor} is annotated @@ -238,9 +240,19 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon extensionContext.getConfigurationParameter(propertyName).orElse(null); return (TestConstructorUtils.isAutowirableConstructor(executable, testClass, junitPropertyProvider) || ApplicationContext.class.isAssignableFrom(parameter.getType()) || + supportsApplicationEvents(parameterContext) || ParameterResolutionDelegate.isAutowirable(parameter, parameterContext.getIndex())); } + private boolean supportsApplicationEvents(ParameterContext parameterContext) { + if (ApplicationEvents.class.isAssignableFrom(parameterContext.getParameter().getType())) { + Assert.isTrue(parameterContext.getDeclaringExecutable() instanceof Method, + "ApplicationEvents can only be injected into test and lifecycle methods"); + return true; + } + return false; + } + /** * Resolve a value for the {@link Parameter} in the supplied {@link ParameterContext} by * retrieving the corresponding dependency from the test's {@link ApplicationContext}. diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java index a80ae973ffc3..164fb5c12d31 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -27,6 +27,7 @@ import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextManager; import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.event.ApplicationEventsTestExecutionListener; import org.springframework.test.context.event.EventPublishingTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener; @@ -54,6 +55,7 @@ *