From 4bf759d8720e5069895506d8fc85c1439cca621f Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 16 Nov 2023 14:07:29 +0000 Subject: [PATCH 001/261] Next development version (v6.0.15-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c54c8a26d3a0..16e9ab72d674 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.14-SNAPSHOT +version=6.0.15-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 86b8a70ce6ae0c8864fe06d7dec0e7c809bc2440 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Mon, 20 Nov 2023 11:27:04 +0100 Subject: [PATCH 002/261] Ensure PathResourceResolvers log warnings for non-existent resources Prior to this commit, the getResource() methods in PathResourceResolver implementations allowed an exception thrown from Resource#getURL() to propagate instead of logging a warning about the missing resource as intended. This commit modifies the getResource() methods in PathResourceResolver implementations so that the log messages include the output of the toString() implementations of the underlying resources instead of their getURL() implementations, which may throw an exception. Furthermore, logging the toString() output of resources aligns with the existing output for "allowed locations" in the same log message. Note that the toString() implementations could potentially also throw exceptions, but that is considered less likely. See gh-31623 Closes gh-31624 (cherry picked from commit 7d2ea7e7e15afb8d3137a1d2667101e4818937ae) --- .../web/reactive/resource/PathResourceResolver.java | 6 +++--- .../web/servlet/resource/PathResourceResolver.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java index dbc86614364b..4b61143b8033 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/PathResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -123,8 +123,8 @@ else if (logger.isWarnEnabled()) { Resource[] allowed = getAllowedLocations(); logger.warn(LogFormatUtils.formatValue( "Resource path \"" + resourcePath + "\" was successfully resolved " + - "but resource \"" + resource.getURL() + "\" is neither under the " + - "current location \"" + location.getURL() + "\" nor under any of the " + + "but resource \"" + resource + "\" is neither under the " + + "current location \"" + location + "\" nor under any of the " + "allowed locations " + (allowed != null ? Arrays.asList(allowed) : "[]"), -1, true)); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java index 6704b98d503a..3902a6ae7a6f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java @@ -195,8 +195,8 @@ else if (logger.isWarnEnabled()) { Resource[] allowed = getAllowedLocations(); logger.warn(LogFormatUtils.formatValue( "Resource path \"" + resourcePath + "\" was successfully resolved " + - "but resource \"" + resource.getURL() + "\" is neither under " + - "the current location \"" + location.getURL() + "\" nor under any of " + + "but resource \"" + resource + "\" is neither under " + + "the current location \"" + location + "\" nor under any of " + "the allowed locations " + (allowed != null ? Arrays.asList(allowed) : "[]"), -1, true)); } } From 0fc38117dffac61b0ed276198dc5231bc5be20f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 Nov 2023 11:44:12 +0100 Subject: [PATCH 003/261] Handle default package with AOT processing Adding generated code in the default package is not supported as we intend to import it, most probably from another package, and that is not supported. While this situation is hard to replicate with Java, Kotlin is unfortunately more lenient and users can end up in that situation if they forget to add a package statement. This commit checks for the presence of a valid package, and throws a dedicated exception if necessary. Closes gh-31629 --- .../aot/generate/GeneratedFiles.java | 11 +++++++++++ .../aot/generate/GeneratedFilesTests.java | 17 +++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java index bd6ec60ae27c..1c326e4fd96d 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java @@ -20,6 +20,7 @@ import org.springframework.javapoet.JavaFile; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.util.function.ThrowingConsumer; /** @@ -43,6 +44,7 @@ public interface GeneratedFiles { * @param javaFile the java file to add */ default void addSourceFile(JavaFile javaFile) { + validatePackage(javaFile.packageName, javaFile.typeSpec.name); String className = javaFile.packageName + "." + javaFile.typeSpec.name; addSourceFile(className, javaFile::writeTo); } @@ -161,11 +163,20 @@ default void addFile(Kind kind, String path, ThrowingConsumer conten private static String getClassNamePath(String className) { Assert.hasLength(className, "'className' must not be empty"); + validatePackage(ClassUtils.getPackageName(className), className); Assert.isTrue(isJavaIdentifier(className), "'className' must be a valid identifier, got '" + className + "'"); return ClassUtils.convertClassNameToResourcePath(className) + ".java"; } + private static void validatePackage(String packageName, String className) { + if (!StringUtils.hasLength(packageName)) { + throw new IllegalArgumentException("Could not add '" + className + "', " + + "processing classes in the default package is not supported. " + + "Did you forget to add a package statement?"); + } + } + private static boolean isJavaIdentifier(String className) { char[] chars = className.toCharArray(); for (int i = 0; i < chars.length; i++) { diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java index 0eeb510490e9..3372fe0c4dae 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java @@ -60,6 +60,15 @@ void addSourceFileWithJavaFileAddsFile() throws Exception { .contains("Hello, World!"); } + @Test + void addSourceFileWithJavaFileInTheDefaultPackageThrowsException() { + TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld").build(); + JavaFile javaFile = JavaFile.builder("", helloWorld).build(); + assertThatIllegalArgumentException().isThrownBy(() -> this.generatedFiles.addSourceFile(javaFile)) + .withMessage("Could not add 'HelloWorld', processing classes in the " + + "default package is not supported. Did you forget to add a package statement?"); + } + @Test void addSourceFileWithCharSequenceAddsFile() throws Exception { this.generatedFiles.addSourceFile("com.example.HelloWorld", "{}"); @@ -73,6 +82,14 @@ void addSourceFileWithCharSequenceWhenClassNameIsEmptyThrowsException() { .withMessage("'className' must not be empty"); } + @Test + void addSourceFileWithCharSequenceWhenClassNameIsInTheDefaultPackageThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.generatedFiles.addSourceFile("HelloWorld", "{}")) + .withMessage("Could not add 'HelloWorld', processing classes in the " + + "default package is not supported. Did you forget to add a package statement?"); + } + @Test void addSourceFileWithCharSequenceWhenClassNameIsInvalidThrowsException() { assertThatIllegalArgumentException() From 0ee36095e7ca79a1c2e7767b9db92518a5ce98ad Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 20 Nov 2023 21:01:36 +0100 Subject: [PATCH 004/261] Restore outdated local/remote-slsb attributes for declaration compatibility Legacy EJB attributes are ignored since 6.0 due to being bound to a plain JndiObjectFactoryBean - but can still be declared now, e.g. when validating against the common versions of spring-jee.xsd out there. Closes gh-31627 (cherry picked from commit 695559879e5bd7936cc60fba937889b49c9c2f8e) --- ...lStatelessSessionBeanDefinitionParser.java | 11 ++- ...eStatelessSessionBeanDefinitionParser.java | 11 ++- .../springframework/ejb/config/spring-jee.xsd | 81 ++++++++++++++++++- .../ejb/config/JeeNamespaceHandlerTests.java | 36 ++++++++- .../ejb/config/jeeNamespaceHandlerTests.xml | 37 +++++++-- 5 files changed, 163 insertions(+), 13 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java index 035fad6581f5..14f6f88b10a1 100644 --- a/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/ejb/config/LocalStatelessSessionBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,13 @@ import org.w3c.dom.Element; +import org.springframework.beans.BeanUtils; import org.springframework.jndi.JndiObjectFactoryBean; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} * implementation for parsing '{@code local-slsb}' tags and - * creating plain {@link JndiObjectFactoryBean} definitions. + * creating plain {@link JndiObjectFactoryBean} definitions on 6.0. * * @author Rob Harrop * @author Juergen Hoeller @@ -36,4 +37,10 @@ protected Class getBeanClass(Element element) { return JndiObjectFactoryBean.class; } + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + BeanUtils.getPropertyDescriptor(JndiObjectFactoryBean.class, extractPropertyName(attributeName)) != null); + } + } diff --git a/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java index a88c5f646189..9768e0dfc337 100644 --- a/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/ejb/config/RemoteStatelessSessionBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,13 @@ import org.w3c.dom.Element; +import org.springframework.beans.BeanUtils; import org.springframework.jndi.JndiObjectFactoryBean; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} * implementation for parsing '{@code remote-slsb}' tags and - * creating plain {@link JndiObjectFactoryBean} definitions. + * creating plain {@link JndiObjectFactoryBean} definitions as of 6.0. * * @author Rob Harrop * @author Juergen Hoeller @@ -36,4 +37,10 @@ protected Class getBeanClass(Element element) { return JndiObjectFactoryBean.class; } + @Override + protected boolean isEligibleAttribute(String attributeName) { + return (super.isEligibleAttribute(attributeName) && + BeanUtils.getPropertyDescriptor(JndiObjectFactoryBean.class, extractPropertyName(attributeName)) != null); + } + } diff --git a/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd b/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd index 283e803db813..97e05f62415f 100644 --- a/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd +++ b/spring-context/src/main/resources/org/springframework/ejb/config/spring-jee.xsd @@ -95,7 +95,7 @@ - + - + + + + + + + + + + + + + + + + + + + + + + - + + @@ -183,6 +224,40 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java index 65292d305730..862484d7d8f0 100644 --- a/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/ejb/config/JeeNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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,9 +16,12 @@ package org.springframework.ejb.config; +import javax.naming.NoInitialContextException; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -29,6 +32,7 @@ import org.springframework.jndi.JndiObjectFactoryBean; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * @author Rob Harrop @@ -93,6 +97,10 @@ public void testSimpleLocalSlsb() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("simpleLocalEjb"); assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); assertPropertyValue(beanDefinition, "jndiName", "ejb/MyLocalBean"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.beanFactory.getBean("simpleLocalEjb")) + .withCauseInstanceOf(NoInitialContextException.class); } @Test @@ -100,6 +108,32 @@ public void testSimpleRemoteSlsb() { BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("simpleRemoteEjb"); assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); assertPropertyValue(beanDefinition, "jndiName", "ejb/MyRemoteBean"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.beanFactory.getBean("simpleRemoteEjb")) + .withCauseInstanceOf(NoInitialContextException.class); + } + + @Test + public void testComplexLocalSlsb() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("complexLocalEjb"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "ejb/MyLocalBean"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.beanFactory.getBean("complexLocalEjb")) + .withCauseInstanceOf(NoInitialContextException.class); + } + + @Test + public void testComplexRemoteSlsb() { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("complexRemoteEjb"); + assertThat(beanDefinition.getBeanClassName()).isEqualTo(JndiObjectFactoryBean.class.getName()); + assertPropertyValue(beanDefinition, "jndiName", "ejb/MyRemoteBean"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> this.beanFactory.getBean("complexRemoteEjb")) + .withCauseInstanceOf(NoInitialContextException.class); } @Test diff --git a/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml b/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml index e8a47157a877..dd8f402278eb 100644 --- a/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml +++ b/spring-context/src/test/resources/org/springframework/ejb/config/jeeNamespaceHandlerTests.xml @@ -40,14 +40,41 @@ - + + + + foo=bar + - + - + + foo=bar + + + - - + + + + From 65781046cf4b46b0f7d76cd2ea7ef530f108ce56 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 20 Nov 2023 21:01:40 +0100 Subject: [PATCH 005/261] Polishing (cherry picked from commit fff50657d241a2756d885f49b3d5eda1b1c4af55) --- .../config/EnableCachingIntegrationTests.java | 2 +- .../interceptor/CacheErrorHandlerTests.java | 29 ++++++++++--------- .../interceptor/CachePutEvaluationTests.java | 10 +++---- .../CacheResolverCustomizationTests.java | 17 ++++++----- .../web/servlet/config/spring-mvc.xsd | 10 +++---- .../web/socket/config/spring-websocket.xsd | 6 ++-- 6 files changed, 39 insertions(+), 35 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java index f4e25b70caf8..0c3b2181eb21 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingIntegrationTests.java @@ -54,7 +54,7 @@ class EnableCachingIntegrationTests { @AfterEach - public void closeContext() { + void closeContext() { if (this.context != null) { this.context.close(); } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java index 8593ba54b6bd..ade3d831b824 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java @@ -60,6 +60,7 @@ class CacheErrorHandlerTests { private SimpleService simpleService; + @BeforeEach void setup() { this.context = new AnnotationConfigApplicationContext(Config.class); @@ -69,11 +70,13 @@ void setup() { this.simpleService = context.getBean(SimpleService.class); } + @AfterEach - void tearDown() { + void closeContext() { this.context.close(); } + @Test void getFail() { UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); @@ -107,9 +110,9 @@ void getFailProperException() { this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.simpleService.get(0L)) - .withMessage("Test exception on get"); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.simpleService.get(0L)) + .withMessage("Test exception on get"); } @Test @@ -128,9 +131,9 @@ void putFailProperException() { this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.simpleService.put(0L)) - .withMessage("Test exception on put"); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.simpleService.put(0L)) + .withMessage("Test exception on put"); } @Test @@ -149,9 +152,9 @@ void evictFailProperException() { this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.simpleService.evict(0L)) - .withMessage("Test exception on evict"); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.simpleService.evict(0L)) + .withMessage("Test exception on evict"); } @Test @@ -170,9 +173,9 @@ void clearFailProperException() { this.cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - this.simpleService.clear()) - .withMessage("Test exception on clear"); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.simpleService.clear()) + .withMessage("Test exception on clear"); } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java index f80f6b8461c1..3bc0eacdd89e 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CachePutEvaluationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -51,6 +51,7 @@ public class CachePutEvaluationTests { private SimpleService service; + @BeforeEach public void setup() { this.context = new AnnotationConfigApplicationContext(Config.class); @@ -59,12 +60,11 @@ public void setup() { } @AfterEach - public void close() { - if (this.context != null) { - this.context.close(); - } + public void closeContext() { + this.context.close(); } + @Test public void mutualGetPutExclusion() { String key = "1"; diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java index 51200a4408a8..4e5cafd54b9d 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -72,7 +72,7 @@ void setup() { } @AfterEach - void tearDown() { + void closeContext() { this.context.close(); } @@ -142,16 +142,17 @@ void namedResolution() { @Test void noCacheResolved() { Method method = ReflectionUtils.findMethod(SimpleService.class, "noCacheResolved", Object.class); - assertThatIllegalStateException().isThrownBy(() -> - this.simpleService.noCacheResolved(new Object())) - .withMessageContaining(method.toString()); + + assertThatIllegalStateException() + .isThrownBy(() -> this.simpleService.noCacheResolved(new Object())) + .withMessageContaining(method.toString()); } @Test void unknownCacheResolver() { - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - this.simpleService.unknownCacheResolver(new Object())) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("unknownCacheResolver")); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.simpleService.unknownCacheResolver(new Object())) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("unknownCacheResolver")); } 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 db09c916c74e..6a674fcf1794 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 @@ -1350,12 +1350,12 @@ set on the "Access-Control-Allow-Credentials" response header of preflight requests. - NOTE: Be aware that this option establishes a high - level of trust with the configured domains and also increases the surface - attack of the web application by exposing sensitive user-specific - information such as cookies and CSRF tokens. + NOTE: Be aware that this option establishes a high level of trust with + the configured domains and also increases the surface attack of the web + application by exposing sensitive user-specific information such as + cookies and CSRF tokens. - By default this is not set in which case the + By default, this is not set in which case the "Access-Control-Allow-Credentials" header is also not set and credentials are therefore not allowed. ]]> 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 271332f5cb0e..31de8dbb07cb 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 @@ -420,7 +420,7 @@ @@ -453,7 +453,7 @@ Date: Sat, 18 Nov 2023 21:55:10 +0300 Subject: [PATCH 006/261] Skip buffer in StreamUtils#copy(String) (cherry picked from commit 54f87f1ff796eff3b54d61ed4992f86ef93b6483) --- .../main/java/org/springframework/util/StreamUtils.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java index 95368a137d93..99d09eefe1a6 100644 --- a/spring-core/src/main/java/org/springframework/util/StreamUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java @@ -23,8 +23,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; import java.nio.charset.Charset; import org.springframework.lang.Nullable; @@ -133,9 +131,8 @@ public static void copy(String in, Charset charset, OutputStream out) throws IOE Assert.notNull(charset, "No Charset specified"); Assert.notNull(out, "No OutputStream specified"); - Writer writer = new OutputStreamWriter(out, charset); - writer.write(in); - writer.flush(); + out.write(in.getBytes(charset)); + out.flush(); } /** From 6db00e63c7b21593da5bb4fe156acb718e397898 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 21 Nov 2023 17:58:00 +0000 Subject: [PATCH 007/261] WebSocketMessageBrokerStats implements SmartInitializingSingleton Closes gh-26536 --- .../config/WebSocketMessageBrokerStats.java | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketMessageBrokerStats.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketMessageBrokerStats.java index e5aa7675b3a0..8f57cbea4e67 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketMessageBrokerStats.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/WebSocketMessageBrokerStats.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.core.task.TaskExecutor; import org.springframework.lang.Nullable; import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler; @@ -53,7 +54,7 @@ * @author Sam Brannen * @since 4.1 */ -public class WebSocketMessageBrokerStats { +public class WebSocketMessageBrokerStats implements SmartInitializingSingleton { private static final Log logger = LogFactory.getLog(WebSocketMessageBrokerStats.class); @@ -84,24 +85,6 @@ public class WebSocketMessageBrokerStats { public void setSubProtocolWebSocketHandler(SubProtocolWebSocketHandler webSocketHandler) { this.webSocketHandler = webSocketHandler; - this.stompSubProtocolHandler = initStompSubProtocolHandler(); - } - - @Nullable - private StompSubProtocolHandler initStompSubProtocolHandler() { - if (this.webSocketHandler == null) { - return null; - } - for (SubProtocolHandler handler : this.webSocketHandler.getProtocolHandlers()) { - if (handler instanceof StompSubProtocolHandler stompHandler) { - return stompHandler; - } - } - SubProtocolHandler defaultHandler = this.webSocketHandler.getDefaultProtocolHandler(); - if (defaultHandler instanceof StompSubProtocolHandler stompHandler) { - return stompHandler; - } - return null; } public void setStompBrokerRelay(StompBrokerRelayMessageHandler stompBrokerRelay) { @@ -118,17 +101,6 @@ public void setOutboundChannelExecutor(TaskExecutor outboundChannelExecutor) { public void setSockJsTaskScheduler(TaskScheduler sockJsTaskScheduler) { this.sockJsTaskScheduler = sockJsTaskScheduler; - this.loggingTask = initLoggingTask(TimeUnit.MINUTES.toMillis(1)); - } - - @Nullable - private ScheduledFuture initLoggingTask(long initialDelay) { - if (this.sockJsTaskScheduler != null && this.loggingPeriod > 0 && logger.isInfoEnabled()) { - return this.sockJsTaskScheduler.scheduleWithFixedDelay( - () -> logger.info(WebSocketMessageBrokerStats.this.toString()), - Instant.now().plusMillis(initialDelay), Duration.ofMillis(this.loggingPeriod)); - } - return null; } /** @@ -137,11 +109,11 @@ private ScheduledFuture initLoggingTask(long initialDelay) { *

By default this property is set to 30 minutes (30 * 60 * 1000). */ public void setLoggingPeriod(long period) { + this.loggingPeriod = period; if (this.loggingTask != null) { this.loggingTask.cancel(true); + this.loggingTask = initLoggingTask(0); } - this.loggingPeriod = period; - this.loggingTask = initLoggingTask(0); } /** @@ -151,6 +123,41 @@ public long getLoggingPeriod() { return this.loggingPeriod; } + + @Override + public void afterSingletonsInstantiated() { + this.stompSubProtocolHandler = initStompSubProtocolHandler(); + this.loggingTask = initLoggingTask(TimeUnit.MINUTES.toMillis(1)); + } + + @Nullable + private StompSubProtocolHandler initStompSubProtocolHandler() { + if (this.webSocketHandler == null) { + return null; + } + for (SubProtocolHandler handler : this.webSocketHandler.getProtocolHandlers()) { + if (handler instanceof StompSubProtocolHandler stompHandler) { + return stompHandler; + } + } + SubProtocolHandler defaultHandler = this.webSocketHandler.getDefaultProtocolHandler(); + if (defaultHandler instanceof StompSubProtocolHandler stompHandler) { + return stompHandler; + } + return null; + } + + @Nullable + private ScheduledFuture initLoggingTask(long initialDelay) { + if (this.sockJsTaskScheduler != null && this.loggingPeriod > 0 && logger.isInfoEnabled()) { + return this.sockJsTaskScheduler.scheduleWithFixedDelay( + () -> logger.info(WebSocketMessageBrokerStats.this.toString()), + Instant.now().plusMillis(initialDelay), Duration.ofMillis(this.loggingPeriod)); + } + return null; + } + + /** * Get stats about WebSocket sessions. */ From f4ac323409fe42f1db1a2b0150c5925e99c3c57d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 22 Nov 2023 12:38:04 +0100 Subject: [PATCH 008/261] Test for mixed order across bean factory hierarchy See gh-28374 (cherry picked from commit 48f3c0839563cd8999870d90a963326099553e2f) --- .../DefaultListableBeanFactoryTests.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) 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 98f69c44d402..191e85583f43 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 @@ -2102,7 +2102,7 @@ void autowireBeanByTypePrimaryTakesPrecedenceOverPriority() { } @Test - void beanProviderWithParentBeanFactoryReuseOrder() { + void beanProviderWithParentBeanFactoryDetectsOrder() { DefaultListableBeanFactory parentBf = new DefaultListableBeanFactory(); parentBf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); parentBf.registerBeanDefinition("regular", new RootBeanDefinition(TestBean.class)); @@ -2110,10 +2110,36 @@ void beanProviderWithParentBeanFactoryReuseOrder() { lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); lbf.setParentBeanFactory(parentBf); lbf.registerBeanDefinition("low", new RootBeanDefinition(LowPriorityTestBean.class)); + Stream> orderedTypes = lbf.getBeanProvider(TestBean.class).orderedStream().map(Object::getClass); assertThat(orderedTypes).containsExactly(HighPriorityTestBean.class, LowPriorityTestBean.class, TestBean.class); } + @Test // gh-28374 + void beanProviderWithParentBeanFactoryAndMixedOrder() { + DefaultListableBeanFactory parentBf = new DefaultListableBeanFactory(); + parentBf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); + lbf.setParentBeanFactory(parentBf); + + lbf.registerSingleton("plainTestBean", new TestBean()); + + RootBeanDefinition bd1 = new RootBeanDefinition(PriorityTestBeanFactory.class); + bd1.setFactoryMethodName("lowPriorityTestBean"); + lbf.registerBeanDefinition("lowPriorityTestBean", bd1); + + RootBeanDefinition bd2 = new RootBeanDefinition(PriorityTestBeanFactory.class); + bd2.setFactoryMethodName("highPriorityTestBean"); + parentBf.registerBeanDefinition("highPriorityTestBean", bd2); + + ObjectProvider testBeanProvider = lbf.getBeanProvider(ResolvableType.forClass(TestBean.class)); + List resolved = testBeanProvider.orderedStream().toList(); + assertThat(resolved.size()).isEqualTo(3); + assertThat(resolved.get(0)).isSameAs(lbf.getBean("highPriorityTestBean")); + assertThat(resolved.get(1)).isSameAs(lbf.getBean("lowPriorityTestBean")); + assertThat(resolved.get(2)).isSameAs(lbf.getBean("plainTestBean")); + } + @Test void autowireExistingBeanByName() { RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); @@ -3287,6 +3313,18 @@ private static class LowPriorityTestBean extends TestBean { } + private static class PriorityTestBeanFactory { + + public static LowPriorityTestBean lowPriorityTestBean() { + return new LowPriorityTestBean(); + } + + public static HighPriorityTestBean highPriorityTestBean() { + return new HighPriorityTestBean(); + } + } + + private static class NullTestBeanFactoryBean implements FactoryBean { @Override From 2784410cc60506562f5eaba97ba42ea7fca4e65a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 22 Nov 2023 13:01:03 +0100 Subject: [PATCH 009/261] Polishing --- spring-context-support/spring-context-support.gradle | 10 +++++----- .../springframework/cache/annotation/CacheConfig.java | 9 ++++++--- .../springframework/cache/annotation/Cacheable.java | 8 ++++++-- .../org/springframework/jdbc/core/JdbcTemplate.java | 2 +- .../datasource/TransactionAwareDataSourceProxy.java | 4 ++-- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/spring-context-support/spring-context-support.gradle b/spring-context-support/spring-context-support.gradle index 715b922b6809..19e0b97fcede 100644 --- a/spring-context-support/spring-context-support.gradle +++ b/spring-context-support/spring-context-support.gradle @@ -6,12 +6,12 @@ dependencies { api(project(":spring-core")) optional(project(":spring-jdbc")) // for Quartz support optional(project(":spring-tx")) // for Quartz support + optional("com.github.ben-manes.caffeine:caffeine") optional("jakarta.activation:jakarta.activation-api") optional("jakarta.mail:jakarta.mail-api") optional("javax.cache:cache-api") - optional("com.github.ben-manes.caffeine:caffeine") - optional("org.quartz-scheduler:quartz") optional("org.freemarker:freemarker") + optional("org.quartz-scheduler:quartz") testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation("org.assertj:assertj-core") testFixturesImplementation("org.mockito:mockito-core") @@ -20,10 +20,10 @@ dependencies { testImplementation(testFixtures(project(":spring-context"))) testImplementation(testFixtures(project(":spring-core"))) testImplementation(testFixtures(project(":spring-tx"))) - testImplementation("org.hsqldb:hsqldb") testImplementation("jakarta.annotation:jakarta.annotation-api") - testRuntimeOnly("org.ehcache:jcache") + testImplementation("org.hsqldb:hsqldb") + testRuntimeOnly("com.sun.mail:jakarta.mail") testRuntimeOnly("org.ehcache:ehcache") + testRuntimeOnly("org.ehcache:jcache") testRuntimeOnly("org.glassfish:jakarta.el") - testRuntimeOnly("com.sun.mail:jakarta.mail") } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java index 234f353b142d..78da3a22e5ab 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2023 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 @@ * @author Stephane Nicoll * @author Sam Brannen * @since 4.1 + * @see Cacheable */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -42,8 +43,10 @@ * Names of the default caches to consider for caching operations defined * in the annotated class. *

If none is set at the operation level, these are used instead of the default. - *

May be used to determine the target cache (or caches), matching the - * qualifier value or the bean names of a specific bean definition. + *

Names may be used to determine the target cache(s), to be resolved via the + * configured {@link #cacheResolver()} which typically delegates to + * {@link org.springframework.cache.CacheManager#getCache}. + * For further details see {@link Cacheable#cacheNames()}. */ String[] cacheNames() default {}; diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index a207d1f06093..e99218f465ad 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -70,8 +70,12 @@ /** * Names of the caches in which method invocation results are stored. - *

Names may be used to determine the target cache (or caches), matching - * the qualifier value or bean name of a specific bean definition. + *

Names may be used to determine the target cache(s), to be resolved via the + * configured {@link #cacheResolver()} which typically delegates to + * {@link org.springframework.cache.CacheManager#getCache}. + *

This will usually be a single cache name. If multiple names are specified, + * they will be consulted for a cache hit in the order of definition, and they + * will all receive a put/evict request for the same newly cached value. * @since 4.2 * @see #value * @see CacheConfig#cacheNames diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 8859f2202a39..a03381c59814 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -60,7 +60,7 @@ import org.springframework.util.StringUtils; /** - * This is the central class in the JDBC core package. + * This is the central delegate in the JDBC core package. * It simplifies the use of JDBC and helps to avoid common errors. * It executes core JDBC workflow, leaving application code to provide SQL * and extract results. This class executes SQL queries or updates, initiating diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java index b2d18673830b..122ceeba185f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -186,7 +186,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl // Allow for differentiating between the proxy and the raw Connection. StringBuilder sb = new StringBuilder("Transaction-aware proxy for target Connection "); if (this.target != null) { - sb.append('[').append(this.target.toString()).append(']'); + sb.append('[').append(this.target).append(']'); } else { sb.append(" from DataSource [").append(this.targetDataSource).append(']'); From 1f19bb231105b5434ca7a8df7874a00fd9a2a0e7 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 22 Nov 2023 15:32:49 +0000 Subject: [PATCH 010/261] Update STOMP WebSocket transport reference docs Closes gh-31616 --- .../ROOT/pages/web/websocket/server.adoc | 48 ++++++++----------- .../web/websocket/stomp/server-config.adoc | 28 +++++++++-- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc index 653f944dfc08..0e506eaff089 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc @@ -229,34 +229,27 @@ Java initialization API. The following example shows how to do so: [[websocket-server-runtime-configuration]] -== Server Configuration +== Configuring the Server [.small]#xref:web/webflux-websocket.adoc#webflux-websocket-server-config[See equivalent in the Reactive stack]# -Each underlying WebSocket engine exposes configuration properties that control -runtime characteristics, such as the size of message buffer sizes, idle timeout, -and others. +You can configure of the underlying WebSocket server such as input message buffer size, +idle timeout, and more. -For Tomcat, WildFly, and GlassFish, you can add a `ServletServerContainerFactoryBean` to your -WebSocket Java config, as the following example shows: +For Jakarta WebSocket servers, you can add a `ServletServerContainerFactoryBean` to your +Java configuration. For example: [source,java,indent=0,subs="verbatim,quotes"] ---- - @Configuration - @EnableWebSocket - public class WebSocketConfig implements WebSocketConfigurer { - - @Bean - public ServletServerContainerFactoryBean createWebSocketContainer() { - ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); - container.setMaxTextMessageBufferSize(8192); - container.setMaxBinaryMessageBufferSize(8192); - return container; - } - - } + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + container.setMaxTextMessageBufferSize(8192); + container.setMaxBinaryMessageBufferSize(8192); + return container; + } ---- -The following example shows the XML configuration equivalent of the preceding example: +Or to your XML configuration: [source,xml,indent=0,subs="verbatim,quotes,attributes"] ---- @@ -277,12 +270,11 @@ The following example shows the XML configuration equivalent of the preceding ex ---- -NOTE: For client-side WebSocket configuration, you should use `WebSocketContainerFactoryBean` -(XML) or `ContainerProvider.getWebSocketContainer()` (Java configuration). +NOTE: For client Jakarta WebSocket configuration, use +ContainerProvider.getWebSocketContainer() in Java configuration, or +`WebSocketContainerFactoryBean` in XML. -For Jetty, you need to supply a pre-configured Jetty `WebSocketServerFactory` and plug -that into Spring's `DefaultHandshakeHandler` through your WebSocket Java config. -The following example shows how to do so: +For Jetty, you can supply a `Consumer` callback to configure the WebSocket server: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -298,11 +290,9 @@ The following example shows how to do so: @Bean public DefaultHandshakeHandler handshakeHandler() { - WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); policy.setInputBufferSize(8192); policy.setIdleTimeout(600000); - return new DefaultHandshakeHandler( new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); } @@ -349,6 +339,10 @@ The following example shows the XML configuration equivalent of the preceding ex ---- +TIP: When using STOMP over WebSocket, you will also need to configure +xref:web/websocket/stomp/server-config.adoc[STOMP WebSocket transport] +properties. + [[websocket-server-allowed-origins]] diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc index 92538177177f..b5ecf349ae06 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/server-config.adoc @@ -1,9 +1,14 @@ [[websocket-stomp-server-config]] -= WebSocket Server += WebSocket Transport -To configure the underlying WebSocket server, the information in -xref:web/websocket/server.adoc#websocket-server-runtime-configuration[Server Configuration] applies. For Jetty, however you need to set -the `HandshakeHandler` and `WebSocketPolicy` through the `StompEndpointRegistry`: +This section explains how to configure the underlying WebSocket server transport. + +For Jakarta WebSocket servers, add a `ServletServerContainerFactoryBean` to your +configuration. For examples, see +xref:web/websocket/server.adoc#websocket-server-runtime-configuration[Configuring the Server] +under the WebSocket section. + +For Jetty WebSocket servers, customize the `JettyRequestUpgradeStrategy` as follows: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -29,5 +34,20 @@ the `HandshakeHandler` and `WebSocketPolicy` through the `StompEndpointRegistry` } ---- +In addition to WebSocket server properties, there are also STOMP WebSocket transport properties +to customize as follows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @EnableWebSocketMessageBroker + public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registry) { + registry.setMessageSizeLimit(4 * 8192); + registry.setTimeToFirstMessage(30000); + } + } +---- From 8e33805d2965fc33f3915e7089d0d9db6442e273 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 22 Nov 2023 20:56:31 +0100 Subject: [PATCH 011/261] Fix ordering of releasing resources in JSON Encoder Prior to this commit, the Jackson 2.x encoders, in case of encoding a stream of data, would first release the `ByteArrayBuilder` and then the `JsonGenerator`. This order is inconsistent with the single value variant (see `o.s.h.codec.json.AbstractJackson2Encoder#encodeValue`) and invalid since the `JsonGenerator` uses internally the `ByteArrayBuilder`. In case of a CSV Encoder, the codec can buffer data to write the column names of the CSV file. Writing an empty Flux with this Encoder would not fail but still log a NullPointerException ignored by the reactive pipeline. This commit fixes the order and avoid such issues at runtime. Fixes gh-31656 --- spring-web/spring-web.gradle | 1 + .../codec/json/AbstractJackson2Encoder.java | 2 +- .../codec/json/JacksonCsvEncoderTests.java | 101 ++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 spring-web/src/test/java/org/springframework/http/codec/json/JacksonCsvEncoderTests.java diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index f6c9eb6121c8..4477c55f98b8 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -81,6 +81,7 @@ dependencies { testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8") testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin") + testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv") testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("io.micrometer:micrometer-observation-test") testImplementation("io.projectreactor:reactor-test") 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 15b16f521e62..93be40da2402 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 @@ -205,8 +205,8 @@ public Flux encode(Publisher inputStream, DataBufferFactory buffe .doOnNext(dataBuffer -> Hints.touchDataBuffer(dataBuffer, hintsToUse, logger)) .doAfterTerminate(() -> { try { - byteBuilder.release(); generator.close(); + byteBuilder.release(); } catch (IOException ex) { logger.error("Could not close Encoder resources", ex); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/JacksonCsvEncoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/JacksonCsvEncoderTests.java new file mode 100644 index 000000000000..9e4f90b4fc29 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/json/JacksonCsvEncoderTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2023 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.codec.json; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.ResolvableType; +import org.springframework.core.testfixture.codec.AbstractEncoderTests; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; +import org.springframework.web.testfixture.xml.Pojo; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AbstractJackson2Encoder} for the CSV variant and how resources are managed. + * @author Brian Clozel + */ +class JacksonCsvEncoderTests extends AbstractEncoderTests { + + public JacksonCsvEncoderTests() { + super(new JacksonCsvEncoder()); + } + + @Test + @Override + public void canEncode() throws Exception { + ResolvableType pojoType = ResolvableType.forClass(Pojo.class); + assertThat(this.encoder.canEncode(pojoType, JacksonCsvEncoder.TEXT_CSV)).isTrue(); + } + + @Test + @Override + public void encode() throws Exception { + Flux input = Flux.just(new Pojo("spring", "framework"), + new Pojo("spring", "data"), + new Pojo("spring", "boot")); + + testEncode(input, Pojo.class, step -> step + .consumeNextWith(expectString("bar,foo\nframework,spring\n")) + .consumeNextWith(expectString("data,spring\n")) + .consumeNextWith(expectString("boot,spring\n")) + .verifyComplete()); + } + + @Test + // See gh-30493 + // this test did not fail directly but logged a NullPointerException dropped by the reactive pipeline + void encodeEmptyFlux() { + Flux input = Flux.empty(); + testEncode(input, Pojo.class, step -> step.verifyComplete()); + } + + static class JacksonCsvEncoder extends AbstractJackson2Encoder { + public static final MediaType TEXT_CSV = new MediaType("text", "csv"); + + public JacksonCsvEncoder() { + this(CsvMapper.builder().build(), TEXT_CSV); + } + + @Override + protected byte[] getStreamingMediaTypeSeparator(MimeType mimeType) { + // CsvMapper emits newlines + return new byte[0]; + } + + public JacksonCsvEncoder(ObjectMapper mapper, MimeType... mimeTypes) { + super(mapper, mimeTypes); + Assert.isInstanceOf(CsvMapper.class, mapper); + setStreamingMediaTypes(List.of(TEXT_CSV)); + } + + @Override + protected ObjectWriter customizeWriter(ObjectWriter writer, MimeType mimeType, ResolvableType elementType, Map hints) { + var mapper = (CsvMapper) getObjectMapper(); + return writer.with(mapper.schemaFor(elementType.toClass()).withHeader()); + } + } +} From 6121e2f526b587d2c3d78cab753dd87a3c714445 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 23 Nov 2023 15:53:35 +0100 Subject: [PATCH 012/261] Remove API diff Gradle plugin configuration We have not published API diffs for a while now so we should remove the configuration entirely from our build. --- build.gradle | 1 - buildSrc/README.md | 15 -- buildSrc/build.gradle | 7 +- .../build/api/ApiDiffPlugin.java | 141 ------------------ 4 files changed, 1 insertion(+), 163 deletions(-) delete mode 100644 buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java diff --git a/build.gradle b/build.gradle index 6ffc617a46a3..06c34272d397 100644 --- a/build.gradle +++ b/build.gradle @@ -146,7 +146,6 @@ configure(rootProject) { description = "Spring Framework" apply plugin: "io.spring.nohttp" - apply plugin: 'org.springframework.build.api-diff' nohttp { source.exclude "**/test-output/**" diff --git a/buildSrc/README.md b/buildSrc/README.md index 90dfdd23db84..9e35b5b766cf 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -22,21 +22,6 @@ but doesn't affect the classpath of dependent projects. This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly` configurations are preferred. -### API Diff - -This plugin uses the [Gradle JApiCmp](https://github.com/melix/japicmp-gradle-plugin) plugin -to generate API Diff reports for each Spring Framework module. This plugin is applied once on the root -project and creates tasks in each framework module. Unlike previous versions of this part of the build, -there is no need for checking out a specific tag. The plugin will fetch the JARs we want to compare the -current working version with. You can generate the reports for all modules or a single module: - -``` -./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE -./gradlew :spring-core:apiDiff -PbaselineVersion=5.1.0.RELEASE -``` - -The reports are located under `build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/`. - ### RuntimeHints Java Agent diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index e21f9231a98a..cb689b708b8f 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -19,16 +19,11 @@ ext { dependencies { implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}") - implementation "me.champeau.gradle:japicmp-gradle-plugin:0.3.0" - implementation "org.gradle:test-retry-gradle-plugin:1.4.1" + implementation "org.gradle:test-retry-gradle-plugin:1.5.6" } gradlePlugin { plugins { - apiDiffPlugin { - id = "org.springframework.build.api-diff" - implementationClass = "org.springframework.build.api.ApiDiffPlugin" - } conventionsPlugin { id = "org.springframework.build.conventions" implementationClass = "org.springframework.build.ConventionsPlugin" diff --git a/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java b/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java deleted file mode 100644 index 4946191282e3..000000000000 --- a/buildSrc/src/main/java/org/springframework/build/api/ApiDiffPlugin.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2002-2023 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.build.api; - -import java.io.File; -import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; - -import me.champeau.gradle.japicmp.JapicmpPlugin; -import me.champeau.gradle.japicmp.JapicmpTask; -import org.gradle.api.GradleException; -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.Dependency; -import org.gradle.api.plugins.JavaBasePlugin; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.publish.maven.plugins.MavenPublishPlugin; -import org.gradle.api.tasks.TaskProvider; -import org.gradle.jvm.tasks.Jar; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link Plugin} that applies the {@code "japicmp-gradle-plugin"} - * and create tasks for all subprojects named {@code "spring-*"}, diffing the public API one by one - * and creating the reports in {@code "build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/"}. - *

{@code "./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE"} will output the - * reports for the API diff between the baseline version and the current one for all modules. - * You can limit the report to a single module with - * {@code "./gradlew :spring-core:apiDiff -PbaselineVersion=5.1.0.RELEASE"}. - * - * @author Brian Clozel - */ -public class ApiDiffPlugin implements Plugin { - - private static final Logger logger = LoggerFactory.getLogger(ApiDiffPlugin.class); - - public static final String TASK_NAME = "apiDiff"; - - private static final String BASELINE_VERSION_PROPERTY = "baselineVersion"; - - private static final List PACKAGE_INCLUDES = Collections.singletonList("org.springframework.*"); - - private static final URI SPRING_MILESTONE_REPOSITORY = URI.create("https://repo.spring.io/milestone"); - - @Override - public void apply(Project project) { - if (project.hasProperty(BASELINE_VERSION_PROPERTY) && project.equals(project.getRootProject())) { - project.getPluginManager().apply(JapicmpPlugin.class); - project.getPlugins().withType(JapicmpPlugin.class, - plugin -> applyApiDiffConventions(project)); - } - } - - private void applyApiDiffConventions(Project project) { - String baselineVersion = project.property(BASELINE_VERSION_PROPERTY).toString(); - project.subprojects(subProject -> { - if (subProject.getName().startsWith("spring-")) { - createApiDiffTask(baselineVersion, subProject); - } - }); - } - - private void createApiDiffTask(String baselineVersion, Project project) { - if (isProjectEligible(project)) { - // Add Spring Milestone repository for generating diffs against previous milestones - project.getRootProject() - .getRepositories() - .maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(SPRING_MILESTONE_REPOSITORY)); - JapicmpTask apiDiff = project.getTasks().create(TASK_NAME, JapicmpTask.class); - apiDiff.setDescription("Generates an API diff report with japicmp"); - apiDiff.setGroup(JavaBasePlugin.DOCUMENTATION_GROUP); - - apiDiff.setOldClasspath(createBaselineConfiguration(baselineVersion, project)); - TaskProvider jar = project.getTasks().withType(Jar.class).named("jar"); - apiDiff.setNewArchives(project.getLayout().files(jar.get().getArchiveFile().get().getAsFile())); - apiDiff.setNewClasspath(getRuntimeClassPath(project)); - apiDiff.setPackageIncludes(PACKAGE_INCLUDES); - apiDiff.setOnlyModified(true); - apiDiff.setIgnoreMissingClasses(true); - // Ignore Kotlin metadata annotations since they contain - // illegal HTML characters and fail the report generation - apiDiff.setAnnotationExcludes(Collections.singletonList("@kotlin.Metadata")); - - apiDiff.setHtmlOutputFile(getOutputFile(baselineVersion, project)); - - apiDiff.dependsOn(project.getTasks().getByName("jar")); - } - } - - private boolean isProjectEligible(Project project) { - return project.getPlugins().hasPlugin(JavaPlugin.class) - && project.getPlugins().hasPlugin(MavenPublishPlugin.class); - } - - private Configuration createBaselineConfiguration(String baselineVersion, Project project) { - String baseline = String.join(":", - project.getGroup().toString(), project.getName(), baselineVersion); - Dependency baselineDependency = project.getDependencies().create(baseline + "@jar"); - Configuration baselineConfiguration = project.getRootProject().getConfigurations().detachedConfiguration(baselineDependency); - try { - // eagerly resolve the baseline configuration to check whether this is a new Spring module - baselineConfiguration.resolve(); - return baselineConfiguration; - } - catch (GradleException exception) { - logger.warn("Could not resolve {} - assuming this is a new Spring module.", baseline); - } - return project.getRootProject().getConfigurations().detachedConfiguration(); - } - - private Configuration getRuntimeClassPath(Project project) { - return project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); - } - - private File getOutputFile(String baseLineVersion, Project project) { - String buildDirectoryPath = project.getRootProject() - .getLayout().getBuildDirectory().getAsFile().get().getAbsolutePath(); - Path outDir = Paths.get(buildDirectoryPath, "reports", "api-diff", - baseLineVersion + "_to_" + project.getRootProject().getVersion()); - return project.file(outDir.resolve(project.getName() + ".html").toString()); - } - -} \ No newline at end of file From 2baf064d040a73ec1265bed123e255d399a19ab8 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 24 Nov 2023 17:31:37 +0100 Subject: [PATCH 013/261] Add current observation context in ClientRequest Prior to this commit, `ExchangeFilterFunction` could only get the current observation from the reactor context. This is particularly useful when such filters want to add KeyValues to the observation context. This commit makes this use case easier by adding the context of the current observation as a request attribute. This also aligns the behavior with other instrumentations. Fixes gh-31646 --- .../ClientRequestObservationContext.java | 20 +++++++++++++++++++ .../function/client/DefaultWebClient.java | 4 +++- .../client/WebClientObservationTests.java | 20 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientRequestObservationContext.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientRequestObservationContext.java index 45c1ba832ee6..f512ece7a8dd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientRequestObservationContext.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/ClientRequestObservationContext.java @@ -16,6 +16,8 @@ package org.springframework.web.reactive.function.client; +import java.util.Optional; + import io.micrometer.observation.transport.RequestReplySenderContext; import org.springframework.lang.Nullable; @@ -32,6 +34,13 @@ */ public class ClientRequestObservationContext extends RequestReplySenderContext { + /** + * Name of the request attribute holding the {@link ClientRequestObservationContext context} + * for the current observation. + * @since 6.1.1 + */ + public static final String CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE = ClientRequestObservationContext.class.getName(); + @Nullable private String uriTemplate; @@ -96,4 +105,15 @@ public ClientRequest getRequest() { public void setRequest(ClientRequest request) { this.request = request; } + + /** + * Get the current {@link ClientRequestObservationContext observation context} + * from the given request, if available. + * @param request the current client request + * @return the current observation context + * @since 6.1.2 + */ + public static Optional findCurrent(ClientRequest request) { + return Optional.ofNullable((ClientRequestObservationContext) request.attributes().get(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE)); + } } 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 84a738c316ea..91a5b57b5774 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 @@ -450,7 +450,9 @@ public Mono exchange() { if (filterFunctions != null) { filterFunction = filterFunctions.andThen(filterFunction); } - ClientRequest request = requestBuilder.build(); + ClientRequest request = requestBuilder + .attribute(ClientRequestObservationContext.CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE, observation.getContext()) + .build(); observationContext.setUriTemplate((String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null)); observationContext.setRequest(request); Mono responseMono = filterFunction.apply(exchangeFunction) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java index c1fe179a9dc3..4cd8900ce82a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java @@ -152,6 +152,26 @@ public Mono filter(ClientRequest request, ExchangeFunction chain verifyAndGetRequest(); } + @Test + void setsCurrentObservationContextAsRequestAttribute() { + ExchangeFilterFunction assertionFilter = new ExchangeFilterFunction() { + @Override + public Mono filter(ClientRequest request, ExchangeFunction chain) { + Optional observationContext = ClientRequestObservationContext.findCurrent(request); + assertThat(observationContext).isPresent(); + return chain.exchange(request).contextWrite(context -> { + Observation currentObservation = context.get(ObservationThreadLocalAccessor.KEY); + assertThat(currentObservation.getContext()).isEqualTo(observationContext.get()); + return context; + }); + } + }; + this.builder.filter(assertionFilter).build().get().uri("/resource/{id}", 42) + .retrieve().bodyToMono(Void.class) + .block(Duration.ofSeconds(10)); + verifyAndGetRequest(); + } + @Test void recordsObservationWithResponseDetailsWhenFilterFunctionErrors() { ExchangeFilterFunction errorFunction = (req, next) -> next.exchange(req).then(Mono.error(new IllegalStateException())); From aadf96ba9247ca440a0ca9e0c6ecb3c4ba6d8257 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 24 Nov 2023 23:25:01 +0100 Subject: [PATCH 014/261] Properly return loaded type even if identified as reloadable Closes gh-31668 (cherry picked from commit 8921be18de88a0a1e63136c9d77d52eeea0acbe2) --- .../expression/spel/support/StandardTypeLocator.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java index 90e960de6764..816b66cb71c5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java @@ -120,9 +120,10 @@ public Class findType(String typeName) throws EvaluationException { return cachedType; } Class loadedType = loadType(typeName); - if (loadedType != null && - !(this.classLoader instanceof SmartClassLoader scl && scl.isClassReloadable(loadedType))) { - this.typeCache.put(typeName, loadedType); + if (loadedType != null) { + if (!(this.classLoader instanceof SmartClassLoader scl && scl.isClassReloadable(loadedType))) { + this.typeCache.put(typeName, loadedType); + } return loadedType; } throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); From ad44e8ab0a7ec6bb3cff3083fdf6bbb667dbf574 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 24 Nov 2023 23:25:28 +0100 Subject: [PATCH 015/261] Filter candidate methods by name first (for more efficient sorting) Closes gh-28377 (cherry picked from commit 0599320bd81edba1575f39922c586263c6d453b0) --- .../support/ReflectiveMethodResolver.java | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java index 5d756d07d4e2..c4ce19aa3d78 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java @@ -117,6 +117,7 @@ public MethodExecutor resolve(EvaluationContext context, Object targetObject, St TypeConverter typeConverter = context.getTypeConverter(); Class type = (targetObject instanceof Class clazz ? clazz : targetObject.getClass()); ArrayList methods = new ArrayList<>(getMethods(type, targetObject)); + methods.removeIf(method -> !method.getName().equals(name)); // If a filter is registered for this type, call it MethodFilter filter = (this.filters != null ? this.filters.get(type) : null); @@ -160,48 +161,46 @@ else if (m1.isVarArgs() && !m2.isVarArgs()) { boolean multipleOptions = false; for (Method method : methodsToIterate) { - if (method.getName().equals(name)) { - int paramCount = method.getParameterCount(); - List paramDescriptors = new ArrayList<>(paramCount); - for (int i = 0; i < paramCount; i++) { - paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i))); - } - ReflectionHelper.ArgumentsMatchInfo matchInfo = null; - if (method.isVarArgs() && argumentTypes.size() >= (paramCount - 1)) { - // *sigh* complicated - matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter); - } - else if (paramCount == argumentTypes.size()) { - // Name and parameter number match, check the arguments - matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter); + int paramCount = method.getParameterCount(); + List paramDescriptors = new ArrayList<>(paramCount); + for (int i = 0; i < paramCount; i++) { + paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i))); + } + ReflectionHelper.ArgumentsMatchInfo matchInfo = null; + if (method.isVarArgs() && argumentTypes.size() >= (paramCount - 1)) { + // *sigh* complicated + matchInfo = ReflectionHelper.compareArgumentsVarargs(paramDescriptors, argumentTypes, typeConverter); + } + else if (paramCount == argumentTypes.size()) { + // Name and parameter number match, check the arguments + matchInfo = ReflectionHelper.compareArguments(paramDescriptors, argumentTypes, typeConverter); + } + if (matchInfo != null) { + if (matchInfo.isExactMatch()) { + return new ReflectiveMethodExecutor(method, type); } - if (matchInfo != null) { - if (matchInfo.isExactMatch()) { - return new ReflectiveMethodExecutor(method, type); - } - else if (matchInfo.isCloseMatch()) { - if (this.useDistance) { - int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes); - if (closeMatch == null || matchDistance < closeMatchDistance) { - // This is a better match... - closeMatch = method; - closeMatchDistance = matchDistance; - } - } - else { - // Take this as a close match if there isn't one already - if (closeMatch == null) { - closeMatch = method; - } + else if (matchInfo.isCloseMatch()) { + if (this.useDistance) { + int matchDistance = ReflectionHelper.getTypeDifferenceWeight(paramDescriptors, argumentTypes); + if (closeMatch == null || matchDistance < closeMatchDistance) { + // This is a better match... + closeMatch = method; + closeMatchDistance = matchDistance; } } - else if (matchInfo.isMatchRequiringConversion()) { - if (matchRequiringConversion != null) { - multipleOptions = true; + else { + // Take this as a close match if there isn't one already + if (closeMatch == null) { + closeMatch = method; } - matchRequiringConversion = method; } } + else if (matchInfo.isMatchRequiringConversion()) { + if (matchRequiringConversion != null) { + multipleOptions = true; + } + matchRequiringConversion = method; + } } } if (closeMatch != null) { From 6fae3e150e9c07550a20c2a5307af927ba80866b Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 24 Nov 2023 23:25:59 +0100 Subject: [PATCH 016/261] Consider generics in equals method (for ConversionService caching) Closes gh-31672 (cherry picked from commit 710373d28677f9a87629c9ce7a6e12a06c4c7023) --- .../core/convert/TypeDescriptor.java | 2 +- .../core/convert/TypeDescriptorTests.java | 33 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index f02d0ea0a2b9..6a2d78d419ad 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -475,7 +475,7 @@ else if (isMap()) { ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor())); } else { - return true; + return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics()); } } diff --git a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java index 5fe86254899b..79a8daa99b7b 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/TypeDescriptorTests.java @@ -34,11 +34,13 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -663,12 +665,12 @@ void passDownGeneric() throws Exception { } @Test - void upCast() throws Exception { + void upcast() throws Exception { Property property = new Property(getClass(), getClass().getMethod("getProperty"), getClass().getMethod("setProperty", Map.class)); TypeDescriptor typeDescriptor = new TypeDescriptor(property); - TypeDescriptor upCast = typeDescriptor.upcast(Object.class); - assertThat(upCast.getAnnotation(MethodAnnotation1.class)).isNotNull(); + TypeDescriptor upcast = typeDescriptor.upcast(Object.class); + assertThat(upcast.getAnnotation(MethodAnnotation1.class)).isNotNull(); } @Test @@ -682,7 +684,7 @@ void upCastNotSuper() throws Exception { } @Test - void elementTypeForCollectionSubclass() throws Exception { + void elementTypeForCollectionSubclass() { @SuppressWarnings("serial") class CustomSet extends HashSet { } @@ -692,7 +694,7 @@ class CustomSet extends HashSet { } @Test - void elementTypeForMapSubclass() throws Exception { + void elementTypeForMapSubclass() { @SuppressWarnings("serial") class CustomMap extends HashMap { } @@ -704,7 +706,7 @@ class CustomMap extends HashMap { } @Test - void createMapArray() throws Exception { + void createMapArray() { TypeDescriptor mapType = TypeDescriptor.map( LinkedHashMap.class, TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(Integer.class)); TypeDescriptor arrayType = TypeDescriptor.array(mapType); @@ -713,13 +715,13 @@ void createMapArray() throws Exception { } @Test - void createStringArray() throws Exception { + void createStringArray() { TypeDescriptor arrayType = TypeDescriptor.array(TypeDescriptor.valueOf(String.class)); assertThat(TypeDescriptor.valueOf(String[].class)).isEqualTo(arrayType); } @Test - void createNullArray() throws Exception { + void createNullArray() { assertThat((Object) TypeDescriptor.array(null)).isNull(); } @@ -736,13 +738,13 @@ void serializable() throws Exception { } @Test - void createCollectionWithNullElement() throws Exception { + void createCollectionWithNullElement() { TypeDescriptor typeDescriptor = TypeDescriptor.collection(List.class, null); assertThat(typeDescriptor.getElementTypeDescriptor()).isNull(); } @Test - void createMapWithNullElements() throws Exception { + void createMapWithNullElements() { TypeDescriptor typeDescriptor = TypeDescriptor.map(LinkedHashMap.class, null, null); assertThat(typeDescriptor.getMapKeyTypeDescriptor()).isNull(); assertThat(typeDescriptor.getMapValueTypeDescriptor()).isNull(); @@ -757,6 +759,17 @@ void getSource() throws Exception { assertThat(TypeDescriptor.valueOf(Integer.class).getSource()).isEqualTo(Integer.class); } + @Test // gh-31672 + void equalityWithGenerics() { + ResolvableType rt1 = ResolvableType.forClassWithGenerics(Optional.class, Integer.class); + ResolvableType rt2 = ResolvableType.forClassWithGenerics(Optional.class, String.class); + + TypeDescriptor td1 = new TypeDescriptor(rt1, null, null); + TypeDescriptor td2 = new TypeDescriptor(rt2, null, null); + + assertThat(td1).isNotEqualTo(td2); + } + // Methods designed for test introspection From 87730f76b1ffc7bc3fd9258d06cfccb79b259d1a Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 26 Nov 2023 12:09:06 +0100 Subject: [PATCH 017/261] Include scroll() in SharedEntityManagerCreator's queryTerminatingMethods This commit supports the scroll() and scroll(ScrollMode) methods from Hibernate's Query API in SharedEntityManagerCreator's query-terminating methods set. See gh-31682 Closes gh-31683 (cherry picked from commit a15f472898ec4c3e79c30949981da9fe75148240) --- .../org/springframework/orm/jpa/SharedEntityManagerCreator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index 911bbf3ba330..4e32abcd399a 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -88,6 +88,7 @@ public abstract class SharedEntityManagerCreator { "getResultStream", // jakarta.persistence.Query.getResultStream() "getResultList", // jakarta.persistence.Query.getResultList() "list", // org.hibernate.query.Query.list() + "scroll", // org.hibernate.query.Query.scroll() "stream", // org.hibernate.query.Query.stream() "uniqueResult", // org.hibernate.query.Query.uniqueResult() "uniqueResultOptional" // org.hibernate.query.Query.uniqueResultOptional() From 3783d31c09c0c2a7f83961ebb59c4bf03f8fc2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 28 Nov 2023 17:02:09 +0100 Subject: [PATCH 018/261] Quote name attribute if necessary This commit updates MetadataNamingStrategy to quote an ObjectName attribute value if necessary. For now, only the name attribute is handled as it is usually a bean name, and we have no control over its structure. Closes gh-31708 --- .../export/naming/MetadataNamingStrategy.java | 20 +++- .../naming/MetadataNamingStrategyTests.java | 96 +++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 spring-context/src/test/java/org/springframework/jmx/export/naming/MetadataNamingStrategyTests.java diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java index c0a2c4d875e6..ea4792a14b51 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -50,6 +50,9 @@ */ public class MetadataNamingStrategy implements ObjectNamingStrategy, InitializingBean { + private static final char[] QUOTABLE_CHARS = new char[] {',', '=', ':', '"'}; + + /** * The {@code JmxAttributeSource} implementation to use for reading metadata. */ @@ -132,10 +135,23 @@ public ObjectName getObjectName(Object managedBean, @Nullable String beanKey) th } Hashtable properties = new Hashtable<>(); properties.put("type", ClassUtils.getShortName(managedClass)); - properties.put("name", beanKey); + properties.put("name", quoteIfNecessary(beanKey)); return ObjectNameManager.getInstance(domain, properties); } } } + private static String quoteIfNecessary(String value) { + return shouldQuote(value) ? ObjectName.quote(value) : value; + } + + private static boolean shouldQuote(String value) { + for (char quotableChar : QUOTABLE_CHARS) { + if (value.indexOf(quotableChar) != -1) { + return true; + } + } + return false; + } + } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/naming/MetadataNamingStrategyTests.java b/spring-context/src/test/java/org/springframework/jmx/export/naming/MetadataNamingStrategyTests.java new file mode 100644 index 000000000000..d0c13c5c8b73 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/jmx/export/naming/MetadataNamingStrategyTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2023 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.jmx.export.naming; + +import java.util.function.Consumer; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.junit.jupiter.api.Test; + +import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + + +/** + * Tests for {@link MetadataNamingStrategy}. + * + * @author Stephane Nicoll + */ +class MetadataNamingStrategyTests { + + private static final TestBean TEST_BEAN = new TestBean(); + + private final MetadataNamingStrategy strategy; + + MetadataNamingStrategyTests() { + this.strategy = new MetadataNamingStrategy(); + this.strategy.setDefaultDomain("com.example"); + this.strategy.setAttributeSource(new AnnotationJmxAttributeSource()); + } + + @Test + void getObjectNameWhenBeanNameIsSimple() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "myBean"); + assertThat(name.getDomain()).isEqualTo("com.example"); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "myBean")); + } + + @Test + void getObjectNameWhenBeanNameIsValidObjectName() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "com.another:name=myBean"); + assertThat(name.getDomain()).isEqualTo("com.another"); + assertThat(name.getKeyPropertyList()).containsOnly(entry("name", "myBean")); + } + + @Test + void getObjectNameWhenBeanNamContainsComma() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "myBean,"); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "\"myBean,\"")); + } + + @Test + void getObjectNameWhenBeanNamContainsEquals() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "my=Bean"); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "\"my=Bean\"")); + } + + @Test + void getObjectNameWhenBeanNamContainsColon() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "my:Bean"); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "\"my:Bean\"")); + } + + @Test + void getObjectNameWhenBeanNamContainsQuote() throws MalformedObjectNameException { + ObjectName name = this.strategy.getObjectName(TEST_BEAN, "\"myBean\""); + assertThat(name).satisfies(hasDefaultProperties(TEST_BEAN, "\"\\\"myBean\\\"\"")); + } + + private Consumer hasDefaultProperties(Object instance, String expectedName) { + return objectName -> assertThat(objectName.getKeyPropertyList()).containsOnly( + entry("type", ClassUtils.getShortName(instance.getClass())), + entry("name", expectedName)); + } + + static class TestBean {} + +} From edadc7983505186c4fff4f91d2699b37bed76924 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 29 Nov 2023 14:39:56 +0100 Subject: [PATCH 019/261] Fix reactive HTTP server Observation instrumentation Prior to this commit, regressions were introduced with gh-31417: 1. the observation keyvalues would be inconsistent with the HTTP response 2. the observation scope would not cover all controller handlers, causing traceIds to be missing The first issue is caused by the fact that in case of error signals, the observation was stopped before the response was fully committed, which means further processing could happen and update the response status. This commit delays the stop event until the response is committed in case of errors. The second problem is caused by the change from a `contextWrite` operator to using the `tap` operator with a `SignalListener`. The observation was started in the `doOnSubscription` callback, which is too late in some cases. If the WebFlux controller handler is synchronous non-blocking, the execution of the handler is performed before the subscription happens. This means that for those handlers, the observation was not started, even if the current observation was present in the reactor context. This commit changes the `doOnSubscription` to `doFirst` to ensure that the observation is started at the right time. Fixes gh-31715 Fixes gh-31716 --- .../reactive/ServerHttpObservationFilter.java | 33 +++++++++++-------- .../ServerHttpObservationFilterTests.java | 28 ++++++++++++++-- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/filter/reactive/ServerHttpObservationFilter.java b/spring-web/src/main/java/org/springframework/web/filter/reactive/ServerHttpObservationFilter.java index c290926307dd..6f40fa81ca60 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/reactive/ServerHttpObservationFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/reactive/ServerHttpObservationFilter.java @@ -121,16 +121,17 @@ public ObservationSignalListener(ServerRequestObservationContext observationCont DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, observationRegistry); } - @Override - public void doOnSubscription() throws Throwable { - this.observation.start(); - } @Override public Context addToContext(Context originalContext) { return originalContext.put(ObservationThreadLocalAccessor.KEY, this.observation); } + @Override + public void doFirst() throws Throwable { + this.observation.start(); + } + @Override public void doOnCancel() throws Throwable { if (this.observationRecorded.compareAndSet(false, true)) { @@ -142,16 +143,7 @@ public void doOnCancel() throws Throwable { @Override public void doOnComplete() throws Throwable { if (this.observationRecorded.compareAndSet(false, true)) { - ServerHttpResponse response = this.observationContext.getResponse(); - if (response.isCommitted()) { - this.observation.stop(); - } - else { - response.beforeCommit(() -> { - this.observation.stop(); - return Mono.empty(); - }); - } + doOnTerminate(this.observationContext); } } @@ -162,8 +154,21 @@ public void doOnError(Throwable error) throws Throwable { this.observationContext.setConnectionAborted(true); } this.observationContext.setError(error); + doOnTerminate(this.observationContext); + } + } + + private void doOnTerminate(ServerRequestObservationContext context) { + ServerHttpResponse response = context.getResponse(); + if (response.isCommitted()) { this.observation.stop(); } + else { + response.beforeCommit(() -> { + this.observation.stop(); + return Mono.empty(); + }); + } } } diff --git a/spring-web/src/test/java/org/springframework/web/filter/reactive/ServerHttpObservationFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/reactive/ServerHttpObservationFilterTests.java index 4bef48c1149d..9a68eb862e45 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/reactive/ServerHttpObservationFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/reactive/ServerHttpObservationFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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.util.Optional; +import io.micrometer.observation.Observation; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; @@ -27,6 +28,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.observation.ServerRequestObservationContext; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; @@ -65,7 +67,10 @@ void filterShouldAddNewObservationToReactorContext() { ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/test/resource")); exchange.getResponse().setRawStatusCode(200); WebFilterChain filterChain = webExchange -> Mono.deferContextual(contextView -> { - assertThat(contextView.getOrEmpty(ObservationThreadLocalAccessor.KEY)).isPresent(); + Observation observation = contextView.get(ObservationThreadLocalAccessor.KEY); + assertThat(observation).isNotNull(); + // check that the observation was started + assertThat(observation.getContext().getLowCardinalityKeyValue("outcome")).isNotNull(); return Mono.empty(); }); this.filter.filter(exchange, filterChain).block(); @@ -99,6 +104,25 @@ void filterShouldRecordObservationWhenCancelled() { assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN"); } + @Test + void filterShouldStopObservationOnResponseCommit() { + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.post("/test/resource")); + WebFilterChain filterChain = createFilterChain(filterExchange -> { + throw new IllegalArgumentException("server error"); + }); + StepVerifier.create(this.filter.filter(exchange, filterChain).doOnError(throwable -> { + ServerHttpResponse response = exchange.getResponse(); + response.setRawStatusCode(500); + response.setComplete().block(); + })) + .expectError(IllegalArgumentException.class) + .verify(); + Optional observationContext = ServerHttpObservationFilter.findObservationContext(exchange); + assertThat(observationContext.get().getError()).isInstanceOf(IllegalArgumentException.class); + assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SERVER_ERROR"); + } + + private WebFilterChain createFilterChain(ThrowingConsumer exchangeConsumer) { return filterExchange -> { try { From 10391586d12c94026c79563d869f36d84787f42e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 30 Nov 2023 14:16:05 +0100 Subject: [PATCH 020/261] PathEditor considers single-letter URI scheme as NIO path candidate Closes gh-29881 (cherry picked from commit c56c3045364a93d135309536cc905e56a2eae38d) --- .../beans/propertyeditors/PathEditor.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java index 13226e6ca0db..70eb403d6a3d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -78,8 +78,10 @@ public void setAsText(String text) throws IllegalArgumentException { if (nioPathCandidate && !text.startsWith("/")) { try { URI uri = ResourceUtils.toURI(text); - if (uri.getScheme() != null) { - nioPathCandidate = false; + String scheme = uri.getScheme(); + if (scheme != null) { + // No NIO candidate except for "C:" style drive letters + nioPathCandidate = (scheme.length() == 1); // Let's try NIO file system providers via Paths.get(URI) setValue(Paths.get(uri).normalize()); return; @@ -109,7 +111,8 @@ else if (nioPathCandidate && !resource.exists()) { setValue(resource.getFile().toPath()); } catch (IOException ex) { - throw new IllegalArgumentException("Failed to retrieve file for " + resource, ex); + throw new IllegalArgumentException( + "Could not retrieve file for " + resource + ": " + ex.getMessage()); } } } From 2d255c4d5e7fb1f4e73deecb1a6abc96294d6b40 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 1 Dec 2023 15:37:55 +0100 Subject: [PATCH 021/261] Upgrade to Gradle 8.5 Closes gh-31734 (cherry picked from commit 3a53446a2beb4505fb2bede94b52aa6ecfa38391) --- gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862f753..1af9e0930b89 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 21793b4f93915486741d1eb112646a52317e9dc6 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 1 Dec 2023 15:44:37 +0100 Subject: [PATCH 022/261] Suppress warnings in Gradle build (cherry picked from commit c05b4ce776aa196c5a3e1ecc3cf1e1e0299e203b) --- .../web/servlet/handler/HandlerMappingIntrospectorTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index 600ae8b88655..8037a5cad579 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -399,6 +399,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) } + @SuppressWarnings("serial") private static class TestServlet extends HttpServlet { @Override From a1471a9266237293e99c01fc16caea5900b3ba81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 6 Dec 2023 12:18:42 +0100 Subject: [PATCH 023/261] Document `@ModelAttribute` usage with native images Closes gh-31767 --- .../controller/ann-methods/modelattrib-method-args.adoc | 5 ++++- .../mvc-controller/ann-methods/modelattrib-method-args.adoc | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc index 347a025545a9..cc95a89739eb 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc @@ -166,4 +166,7 @@ By default, any argument that is not a simple value type (as determined by and is not resolved by any other argument resolver is treated as if it were annotated with `@ModelAttribute`. - +WARNING: When compiling to native images, implicit `@ModelAttribute` as described above does +not allow proper ahead-of-time inference of related data binding reflection hints. As a consequence, +it is recommended to annotate explicitly with `@ModelAttribute` method parameters for such use case +with GraalVM. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc index 1136ab949588..67db7d4f57dc 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc @@ -216,4 +216,7 @@ By default, any argument that is not a simple value type (as determined by and is not resolved by any other argument resolver is treated as if it were annotated with `@ModelAttribute`. - +WARNING: When compiling to native images, implicit `@ModelAttribute` as described above does +not allow proper ahead-of-time inference of related data binding reflection hints. As a consequence, +it is recommended to annotate explicitly with `@ModelAttribute` method parameters for such use case +with GraalVM. From 29a39b617e8d375d196efebca74c70f435f357a6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 6 Dec 2023 11:51:48 +0100 Subject: [PATCH 024/261] Support empty part in DefaultPartHttpMessageReader This commit fixes a bug in DefaultPartHttpMessageReader's MultipartParser, due to which the last token in a part window was not properly indicated. See gh-30953 Closes gh-31766 --- .../http/codec/multipart/MultipartParser.java | 2 +- .../DefaultPartHttpMessageReaderTests.java | 17 +++++++++++++++++ .../http/codec/multipart/empty-part.multipart | 13 +++++++++++++ ...MultipartRouterFunctionIntegrationTests.java | 8 +++++++- 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 spring-web/src/test/resources/org/springframework/http/codec/multipart/empty-part.multipart diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java index 1b59f6ddf1ea..04fa69770f67 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartParser.java @@ -540,7 +540,7 @@ else if (len < 0) { while ((prev = this.queue.pollLast()) != null) { int prevByteCount = prev.readableByteCount(); int prevLen = prevByteCount + len; - if (prevLen > 0) { + if (prevLen >= 0) { // slice body part of previous buffer, and flush it DataBuffer body = prev.split(prevLen + prev.readPosition()); DataBufferUtils.release(prev); diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java index 1c9596fb5b1a..ac7ec85e944e 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java @@ -301,6 +301,23 @@ void exceedHeaderLimit() throws InterruptedException { latch.await(); } + @ParameterizedDefaultPartHttpMessageReaderTest + void emptyLastPart(DefaultPartHttpMessageReader reader) throws InterruptedException { + MockServerHttpRequest request = createRequest( + new ClassPathResource("empty-part.multipart", getClass()), "LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD"); + + Flux result = reader.read(forClass(Part.class), request, emptyMap()); + + CountDownLatch latch = new CountDownLatch(2); + StepVerifier.create(result) + .consumeNextWith(part -> testPart(part, null, "", latch)) + .consumeNextWith(part -> testPart(part, null, "", latch)) + .verifyComplete(); + + latch.await(); + } + + private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary) throws InterruptedException { diff --git a/spring-web/src/test/resources/org/springframework/http/codec/multipart/empty-part.multipart b/spring-web/src/test/resources/org/springframework/http/codec/multipart/empty-part.multipart new file mode 100644 index 000000000000..501388b78196 --- /dev/null +++ b/spring-web/src/test/resources/org/springframework/http/codec/multipart/empty-part.multipart @@ -0,0 +1,13 @@ +--LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD +Content-Disposition: form-data; name="files"; filename="file17312898095703516893.tmp" +Content-Type: application/octet-stream +Content-Length: 0 + + +--LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD +Content-Disposition: form-data; name="files"; filename="file14790463448453253614.tmp" +Content-Type: application/octet-stream +Content-Length: 0 + + +--LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD-- diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java index 89659613dd64..51b4917570c9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java @@ -259,13 +259,19 @@ public Mono partData(ServerRequest request) { assertThat(data).hasSize(2); List fileData = data.get(0); - assertThat(fileData).hasSize(1); + assertThat(fileData).hasSize(2); assertThat(fileData.get(0)).isInstanceOf(FilePartEvent.class); FilePartEvent filePartEvent = (FilePartEvent) fileData.get(0); assertThat(filePartEvent.name()).isEqualTo("fooPart"); assertThat(filePartEvent.filename()).isEqualTo("foo.txt"); DataBufferUtils.release(filePartEvent.content()); + assertThat(fileData.get(1)).isInstanceOf(FilePartEvent.class); + filePartEvent = (FilePartEvent) fileData.get(1); + assertThat(filePartEvent.name()).isEqualTo("fooPart"); + assertThat(filePartEvent.filename()).isEqualTo("foo.txt"); + DataBufferUtils.release(filePartEvent.content()); + List fieldData = data.get(1); assertThat(fieldData).hasSize(1); assertThat(fieldData.get(0)).isInstanceOf(FormPartEvent.class); From 035cc72fc89979cd57a2f3b8641cab401215f37b Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 6 Dec 2023 12:34:56 +0100 Subject: [PATCH 025/261] Polishing --- .../controller/ann-methods/modelattrib-method-args.adoc | 8 ++++---- .../ann-methods/modelattrib-method-args.adoc | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc index cc95a89739eb..c073dca32efb 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/modelattrib-method-args.adoc @@ -166,7 +166,7 @@ By default, any argument that is not a simple value type (as determined by and is not resolved by any other argument resolver is treated as if it were annotated with `@ModelAttribute`. -WARNING: When compiling to native images, implicit `@ModelAttribute` as described above does -not allow proper ahead-of-time inference of related data binding reflection hints. As a consequence, -it is recommended to annotate explicitly with `@ModelAttribute` method parameters for such use case -with GraalVM. +WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute` +support described above does not allow proper ahead-of-time inference of related data +binding reflection hints. As a consequence, it is recommended to explicitly annotate +method parameters with `@ModelAttribute` for use in a GraalVM native image. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc index 67db7d4f57dc..1414f5cea59e 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc @@ -216,7 +216,7 @@ By default, any argument that is not a simple value type (as determined by and is not resolved by any other argument resolver is treated as if it were annotated with `@ModelAttribute`. -WARNING: When compiling to native images, implicit `@ModelAttribute` as described above does -not allow proper ahead-of-time inference of related data binding reflection hints. As a consequence, -it is recommended to annotate explicitly with `@ModelAttribute` method parameters for such use case -with GraalVM. +WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute` +support described above does not allow proper ahead-of-time inference of related data +binding reflection hints. As a consequence, it is recommended to explicitly annotate +method parameters with `@ModelAttribute` for use in a GraalVM native image. From fa95f12be00ff78234a4d7c195ca06394b576986 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Wed, 6 Dec 2023 21:41:16 +0900 Subject: [PATCH 026/261] Fix condition for "Too many elements" in MimeTypeUtils.sortBySpecificity() See gh-31254 See gh-31769 Closes gh-31773 (cherry picked from commit 7b95bd72f7e9922f655c582f47c2fe80d8664a1b) --- .../src/main/java/org/springframework/util/MimeTypeUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index fb23809994dd..54e5fd00293b 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -362,7 +362,7 @@ public static String toString(Collection mimeTypes) { */ public static void sortBySpecificity(List mimeTypes) { Assert.notNull(mimeTypes, "'mimeTypes' must not be null"); - if (mimeTypes.size() >= 50) { + if (mimeTypes.size() > 50) { throw new InvalidMimeTypeException(mimeTypes.toString(), "Too many elements"); } From 85cc22906306c516848154039adbf64e5c149ad0 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Wed, 6 Dec 2023 14:24:06 +0100 Subject: [PATCH 027/261] Fix and polish Javadoc for MimeTypeUtils (cherry picked from commit 1afea0b144e3fabf138f5ebb4b9f84f75f7d279a) --- .../springframework/util/MimeTypeUtils.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 54e5fd00293b..494d3fcc77d7 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -330,10 +330,10 @@ public static List tokenize(String mimeTypes) { } /** - * Return a string representation of the given list of {@code MimeType} objects. - * @param mimeTypes the string to parse - * @return the list of mime types - * @throws IllegalArgumentException if the String cannot be parsed + * Generate a string representation of the given collection of {@link MimeType} + * objects. + * @param mimeTypes the {@code MimeType} objects + * @return a string representation of the {@code MimeType} objects */ public static String toString(Collection mimeTypes) { StringBuilder builder = new StringBuilder(); @@ -348,14 +348,12 @@ public static String toString(Collection mimeTypes) { } /** - * Sorts the given list of {@code MimeType} objects by + * Sort the given list of {@code MimeType} objects by * {@linkplain MimeType#isMoreSpecific(MimeType) specificity}. - * - *

Because of the computational cost, this method throws an exception - * when the given list contains too many elements. + *

Because of the computational cost, this method throws an exception if + * the given list contains too many elements. * @param mimeTypes the list of mime types to be sorted - * @throws IllegalArgumentException if {@code mimeTypes} contains more - * than 50 elements + * @throws InvalidMimeTypeException if {@code mimeTypes} contains more than 50 elements * @see HTTP 1.1: Semantics * and Content, section 5.3.2 * @see MimeType#isMoreSpecific(MimeType) From dd3a67c7ab6b0417e5ed7a66ab97ee59c10edf85 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 6 Dec 2023 14:24:18 +0100 Subject: [PATCH 028/261] Process tokens after each feed in Jackson2Tokenizer This commit ensures that we process after each fed buffer in Jackson2Tokenizer, instead of after all fed buffers. See gh-31747 Closes gh-31772 --- .../http/codec/json/Jackson2Tokenizer.java | 21 ++++---- .../codec/json/Jackson2TokenizerTests.java | 50 +++++++++++++++---- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index 9b56435503a8..427d8025a7ad 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -91,10 +91,12 @@ private Jackson2Tokenizer(JsonParser parser, DeserializationContext deserializat private List tokenize(DataBuffer dataBuffer) { try { int bufferSize = dataBuffer.readableByteCount(); + List tokens = new ArrayList<>(); if (this.inputFeeder instanceof ByteBufferFeeder byteBufferFeeder) { try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) { while (iterator.hasNext()) { byteBufferFeeder.feedInput(iterator.next()); + parseTokens(tokens); } } } @@ -102,10 +104,10 @@ else if (this.inputFeeder instanceof ByteArrayFeeder byteArrayFeeder) { byte[] bytes = new byte[bufferSize]; dataBuffer.read(bytes); byteArrayFeeder.feedInput(bytes, 0, bufferSize); + parseTokens(tokens); } - List result = parseTokenBufferFlux(); - assertInMemorySize(bufferSize, result); - return result; + assertInMemorySize(bufferSize, tokens); + return tokens; } catch (JsonProcessingException ex) { throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex); @@ -122,7 +124,9 @@ private Flux endOfInput() { return Flux.defer(() -> { this.inputFeeder.endOfInput(); try { - return Flux.fromIterable(parseTokenBufferFlux()); + List tokens = new ArrayList<>(); + parseTokens(tokens); + return Flux.fromIterable(tokens); } catch (JsonProcessingException ex) { throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex); @@ -133,9 +137,7 @@ private Flux endOfInput() { }); } - private List parseTokenBufferFlux() throws IOException { - List result = new ArrayList<>(); - + private void parseTokens(List tokens) throws IOException { // SPR-16151: Smile data format uses null to separate documents boolean previousNull = false; while (!this.parser.isClosed()) { @@ -153,13 +155,12 @@ else if (token == null ) { // !previousNull } updateDepth(token); if (!this.tokenizeArrayElements) { - processTokenNormal(token, result); + processTokenNormal(token, tokens); } else { - processTokenArray(token, result); + processTokenArray(token, tokens); } } - return result; } private void updateDepth(JsonToken token) { diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java index 31641b9347d2..ab9820fa83ba 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java @@ -27,6 +27,10 @@ import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.util.TokenBuffer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,6 +43,7 @@ import org.springframework.core.codec.DecodingException; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferLimitException; +import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests; import static java.util.Arrays.asList; @@ -345,22 +350,47 @@ public void useBigDecimalForFloats(boolean useBigDecimalForFloats) { .verifyComplete(); } + // gh-31747 + @Test + public void compositeNettyBuffer() { + ByteBufAllocator allocator = UnpooledByteBufAllocator.DEFAULT; + ByteBuf firstByteBuf = allocator.buffer(); + firstByteBuf.writeBytes("{\"foo\": \"foofoo\"".getBytes(StandardCharsets.UTF_8)); + ByteBuf secondBuf = allocator.buffer(); + secondBuf.writeBytes(", \"bar\": \"barbar\"}".getBytes(StandardCharsets.UTF_8)); + CompositeByteBuf composite = allocator.compositeBuffer(); + composite.addComponent(true, firstByteBuf); + composite.addComponent(true, secondBuf); + + NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(allocator); + Flux source = Flux.just(bufferFactory.wrap(composite)); + Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, false, -1); + + Flux strings = tokens.map(this::tokenToString); + + StepVerifier.create(strings) + .assertNext(s -> assertThat(s).isEqualTo("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}")) + .verifyComplete(); + } + + private Flux decode(List source, boolean tokenize, int maxInMemorySize) { Flux tokens = Jackson2Tokenizer.tokenize( Flux.fromIterable(source).map(this::stringBuffer), this.jsonFactory, this.objectMapper, tokenize, false, maxInMemorySize); - return tokens - .map(tokenBuffer -> { - try { - TreeNode root = this.objectMapper.readTree(tokenBuffer.asParser()); - return this.objectMapper.writeValueAsString(root); - } - catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }); + return tokens.map(this::tokenToString); + } + + private String tokenToString(TokenBuffer tokenBuffer) { + try { + TreeNode root = this.objectMapper.readTree(tokenBuffer.asParser()); + return this.objectMapper.writeValueAsString(root); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } } private DataBuffer stringBuffer(String value) { From 627d9cf8bee4c419bcd54bbcff5766586e0a74b1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 10 Dec 2023 00:26:22 +0100 Subject: [PATCH 029/261] Polishing --- .../AutowiredAnnotationBeanPostProcessor.java | 29 ++++++++------ .../support/SimpleInstantiationStrategy.java | 6 +-- .../cache/interceptor/CacheAspectSupport.java | 6 +-- ...athScanningCandidateComponentProvider.java | 22 +++++----- .../org/springframework/util/StreamUtils.java | 2 +- .../core/metadata/CallMetaDataProvider.java | 40 +++++++++---------- .../metadata/GenericCallMetaDataProvider.java | 10 ++--- .../jdbc/datasource/AbstractDataSource.java | 20 +++------- .../jdbc/datasource/DelegatingDataSource.java | 36 ++++++----------- .../lookup/AbstractRoutingDataSource.java | 5 ++- ...ersistenceAnnotationBeanPostProcessor.java | 10 ++--- 11 files changed, 86 insertions(+), 100 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index 17e556315972..b071f7c46b23 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -159,6 +159,9 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor, PriorityOrdered, BeanFactoryAware { + private static final Constructor[] EMPTY_CONSTRUCTOR_ARRAY = new Constructor[0]; + + protected final Log logger = LogFactory.getLog(getClass()); private final Set> autowiredAnnotationTypes = new LinkedHashSet<>(4); @@ -193,9 +196,10 @@ public AutowiredAnnotationBeanPostProcessor() { this.autowiredAnnotationTypes.add(Autowired.class); this.autowiredAnnotationTypes.add(Value.class); + ClassLoader classLoader = AutowiredAnnotationBeanPostProcessor.class.getClassLoader(); try { this.autowiredAnnotationTypes.add((Class) - ClassUtils.forName("jakarta.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); + ClassUtils.forName("jakarta.inject.Inject", classLoader)); logger.trace("'jakarta.inject.Inject' annotation found and supported for autowiring"); } catch (ClassNotFoundException ex) { @@ -204,7 +208,7 @@ public AutowiredAnnotationBeanPostProcessor() { try { this.autowiredAnnotationTypes.add((Class) - ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader())); + ClassUtils.forName("javax.inject.Inject", classLoader)); logger.trace("'javax.inject.Inject' annotation found and supported for autowiring"); } catch (ClassNotFoundException ex) { @@ -285,9 +289,16 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + // Register externally managed config members on bean definition. findInjectionMetadata(beanName, beanType, beanDefinition); } + @Override + public void resetBeanDefinition(String beanName) { + this.lookupMethodsChecked.remove(beanName); + this.injectionMetadataCache.remove(beanName); + } + @Override @Nullable public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { @@ -323,12 +334,6 @@ private InjectionMetadata findInjectionMetadata(String beanName, Class beanTy return metadata; } - @Override - public void resetBeanDefinition(String beanName) { - this.lookupMethodsChecked.remove(beanName); - this.injectionMetadataCache.remove(beanName); - } - @Override public Class determineBeanType(Class beanClass, String beanName) throws BeanCreationException { checkLookupMethods(beanClass, beanName); @@ -428,7 +433,7 @@ else if (candidates.size() == 1 && logger.isInfoEnabled()) { "default constructor to fall back to: " + candidates.get(0)); } } - candidateConstructors = candidates.toArray(new Constructor[0]); + candidateConstructors = candidates.toArray(EMPTY_CONSTRUCTOR_ARRAY); } else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) { candidateConstructors = new Constructor[] {rawCandidates[0]}; @@ -441,7 +446,7 @@ else if (nonSyntheticConstructors == 1 && primaryConstructor != null) { candidateConstructors = new Constructor[] {primaryConstructor}; } else { - candidateConstructors = new Constructor[0]; + candidateConstructors = EMPTY_CONSTRUCTOR_ARRAY; } this.candidateConstructorsCache.put(beanClass, candidateConstructors); } @@ -1011,7 +1016,7 @@ private CodeBlock generateMethodStatementForField(ClassName targetClassName, hints.reflection().registerField(field); CodeBlock resolver = CodeBlock.of("$T.$L($S)", AutowiredFieldValueResolver.class, - (!required) ? "forField" : "forRequiredField", field.getName()); + (!required ? "forField" : "forRequiredField"), field.getName()); AccessControl accessControl = AccessControl.forMember(field); if (!accessControl.isAccessibleFrom(targetClassName)) { return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, @@ -1026,7 +1031,7 @@ private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, CodeBlock.Builder code = CodeBlock.builder(); code.add("$T.$L", AutowiredMethodArgumentsResolver.class, - (!required) ? "forMethod" : "forRequiredMethod"); + (!required ? "forMethod" : "forRequiredMethod")); code.add("($S", method.getName()); if (method.getParameterCount() > 0) { code.add(", $L", generateParameterTypesCode(method.getParameterTypes())); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java index 9efbc3b9b8c2..0fc15c5a3539 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java @@ -71,7 +71,7 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean synchronized (bd.constructorArgumentLock) { constructorToUse = (Constructor) bd.resolvedConstructorOrFactoryMethod; if (constructorToUse == null) { - final Class clazz = bd.getBeanClass(); + Class clazz = bd.getBeanClass(); if (clazz.isInterface()) { throw new BeanInstantiationException(clazz, "Specified class is an interface"); } @@ -104,7 +104,7 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - final Constructor ctor, Object... args) { + Constructor ctor, Object... args) { if (!bd.hasMethodOverrides()) { return BeanUtils.instantiateClass(ctor, args); @@ -128,7 +128,7 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - @Nullable Object factoryBean, final Method factoryMethod, Object... args) { + @Nullable Object factoryBean, Method factoryMethod, Object... args) { try { ReflectionUtils.makeAccessible(factoryMethod); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index d99e31d87c2d..3dc8360f6ee9 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -547,10 +547,10 @@ private Cache.ValueWrapper findCachedItem(Collection cont } /** - * Collect the {@link CachePutRequest} for all {@link CacheOperation} using - * the specified result value. + * Collect a {@link CachePutRequest} for every {@link CacheOperation} + * using the specified result value. * @param contexts the contexts to handle - * @param result the result value (never {@code null}) + * @param result the result value * @param putRequests the collection to update */ private void collectPutRequests(Collection contexts, diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index 7c717714c75a..3461dcdbc63e 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,11 +62,13 @@ import org.springframework.util.ClassUtils; /** - * A component provider that provides candidate components from a base package. Can - * use {@link CandidateComponentsIndex the index} if it is available of scans the - * classpath otherwise. Candidate components are identified by applying exclude and - * include filters. {@link AnnotationTypeFilter}, {@link AssignableTypeFilter} include - * filters on an annotation/superclass that are annotated with {@link Indexed} are + * A component provider that scans for candidate components starting from a + * specified base package. Can use the {@linkplain CandidateComponentsIndex component + * index}, if it is available, and scans the classpath otherwise. + * + *

Candidate components are identified by applying exclude and include filters. + * {@link AnnotationTypeFilter} and {@link AssignableTypeFilter} include filters + * for an annotation/target-type that is annotated with {@link Indexed} are * supported: if any other include filter is specified, the index is ignored and * classpath scanning is used instead. * @@ -201,7 +203,6 @@ public void resetFilters(boolean useDefaultFilters) { * {@link Controller @Controller} stereotype annotations. *

Also supports Jakarta EE's {@link jakarta.annotation.ManagedBean} and * JSR-330's {@link jakarta.inject.Named} annotations, if available. - * */ @SuppressWarnings("unchecked") protected void registerDefaultFilters() { @@ -305,7 +306,7 @@ public final MetadataReaderFactory getMetadataReaderFactory() { /** - * Scan the class path for candidate components. + * Scan the component index or class path for candidate components. * @param basePackage the package to check for annotated classes * @return a corresponding Set of autodetected bean definitions */ @@ -319,7 +320,7 @@ public Set findCandidateComponents(String basePackage) { } /** - * Determine if the index can be used by this instance. + * Determine if the component index can be used by this instance. * @return {@code true} if the index is available and the configuration of this * instance is supported by it, {@code false} otherwise * @since 5.0 @@ -460,8 +461,7 @@ private Set scanCandidateComponents(String basePackage) { } } catch (Throwable ex) { - throw new BeanDefinitionStoreException( - "Failed to read candidate component class: " + resource, ex); + throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex); } } } diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java index 99d09eefe1a6..9d982f89a6f9 100644 --- a/spring-core/src/main/java/org/springframework/util/StreamUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java index 068a1c457813..1666daf420ae 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -43,7 +43,7 @@ public interface CallMetaDataProvider { /** * Initialize the database specific management of procedure column meta-data. - * This is only called for databases that are supported. This initialization + *

This is only called for databases that are supported. This initialization * can be turned off by specifying that column meta-data should not be used. * @param databaseMetaData used to retrieve database specific information * @param catalogName name of catalog to use (or {@code null} if none) @@ -55,30 +55,36 @@ public interface CallMetaDataProvider { void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @Nullable String catalogName, @Nullable String schemaName, @Nullable String procedureName) throws SQLException; + /** + * Get the call parameter meta-data that is currently used. + * @return a List of {@link CallParameterMetaData} + */ + List getCallParameterMetaData(); + /** * Provide any modification of the procedure name passed in to match the meta-data currently used. - * This could include altering the case. + *

This could include altering the case. */ @Nullable String procedureNameToUse(@Nullable String procedureName); /** * Provide any modification of the catalog name passed in to match the meta-data currently used. - * This could include altering the case. + *

This could include altering the case. */ @Nullable String catalogNameToUse(@Nullable String catalogName); /** * Provide any modification of the schema name passed in to match the meta-data currently used. - * This could include altering the case. + *

This could include altering the case. */ @Nullable String schemaNameToUse(@Nullable String schemaName); /** * Provide any modification of the catalog name passed in to match the meta-data currently used. - * The returned value will be used for meta-data lookups. This could include altering the case + *

The returned value will be used for meta-data lookups. This could include altering the case * used or providing a base catalog if none is provided. */ @Nullable @@ -86,7 +92,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Provide any modification of the schema name passed in to match the meta-data currently used. - * The returned value will be used for meta-data lookups. This could include altering the case + *

The returned value will be used for meta-data lookups. This could include altering the case * used or providing a base schema if none is provided. */ @Nullable @@ -94,7 +100,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Provide any modification of the column name passed in to match the meta-data currently used. - * This could include altering the case. + *

This could include altering the case. * @param parameterName name of the parameter of column */ @Nullable @@ -102,7 +108,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Create a default out parameter based on the provided meta-data. - * This is used when no explicit parameter declaration has been made. + *

This is used when no explicit parameter declaration has been made. * @param parameterName the name of the parameter * @param meta meta-data used for this call * @return the configured SqlOutParameter @@ -111,7 +117,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Create a default in/out parameter based on the provided meta-data. - * This is used when no explicit parameter declaration has been made. + *

This is used when no explicit parameter declaration has been made. * @param parameterName the name of the parameter * @param meta meta-data used for this call * @return the configured SqlInOutParameter @@ -120,7 +126,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Create a default in parameter based on the provided meta-data. - * This is used when no explicit parameter declaration has been made. + *

This is used when no explicit parameter declaration has been made. * @param parameterName the name of the parameter * @param meta meta-data used for this call * @return the configured SqlParameter @@ -142,7 +148,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N /** * Does this database support returning ResultSets as ref cursors to be retrieved with - * {@link java.sql.CallableStatement#getObject(int)} for the specified column. + * {@link java.sql.CallableStatement#getObject(int)} for the specified column? */ boolean isRefCursorSupported(); @@ -158,18 +164,12 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N boolean isProcedureColumnMetaDataUsed(); /** - * Should we bypass the return parameter with the specified name. - * This allows the database specific implementation to skip the processing + * Should we bypass the return parameter with the specified name? + *

This allows the database specific implementation to skip the processing * for specific results returned by the database call. */ boolean byPassReturnParameter(String parameterName); - /** - * Get the call parameter meta-data that is currently used. - * @return a List of {@link CallParameterMetaData} - */ - List getCallParameterMetaData(); - /** * Does the database support the use of catalog name in procedure calls? */ diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java index 3822fae85baa..789351ff5eee 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java @@ -168,11 +168,6 @@ public String parameterNameToUse(@Nullable String parameterName) { return identifierNameToUse(parameterName); } - @Override - public boolean byPassReturnParameter(String parameterName) { - return false; - } - @Override public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) { return new SqlOutParameter(parameterName, meta.getSqlType()); @@ -213,6 +208,11 @@ public boolean isProcedureColumnMetaDataUsed() { return this.procedureColumnMetaDataUsed; } + @Override + public boolean byPassReturnParameter(String parameterName) { + return false; + } + /** * Specify whether the database supports the use of catalog name in procedure calls. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java index 2bb62d1c3d66..33e6c31d94e6 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2023 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. @@ -75,10 +75,10 @@ public void setLogWriter(PrintWriter pw) throws SQLException { throw new UnsupportedOperationException("setLogWriter"); } - - //--------------------------------------------------------------------- - // Implementation of JDBC 4.0's Wrapper interface - //--------------------------------------------------------------------- + @Override + public Logger getParentLogger() { + return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + } @Override @SuppressWarnings("unchecked") @@ -95,14 +95,4 @@ public boolean isWrapperFor(Class iface) throws SQLException { return iface.isInstance(this); } - - //--------------------------------------------------------------------- - // Implementation of JDBC 4.1's getParentLogger method - //--------------------------------------------------------------------- - - @Override - public Logger getParentLogger() { - return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); - } - } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java index 720439782053..a2a39f3aa180 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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,29 +105,29 @@ public Connection getConnection(String username, String password) throws SQLExce } @Override - public PrintWriter getLogWriter() throws SQLException { - return obtainTargetDataSource().getLogWriter(); + public int getLoginTimeout() throws SQLException { + return obtainTargetDataSource().getLoginTimeout(); } @Override - public void setLogWriter(PrintWriter out) throws SQLException { - obtainTargetDataSource().setLogWriter(out); + public void setLoginTimeout(int seconds) throws SQLException { + obtainTargetDataSource().setLoginTimeout(seconds); } @Override - public int getLoginTimeout() throws SQLException { - return obtainTargetDataSource().getLoginTimeout(); + public PrintWriter getLogWriter() throws SQLException { + return obtainTargetDataSource().getLogWriter(); } @Override - public void setLoginTimeout(int seconds) throws SQLException { - obtainTargetDataSource().setLoginTimeout(seconds); + public void setLogWriter(PrintWriter out) throws SQLException { + obtainTargetDataSource().setLogWriter(out); } - - //--------------------------------------------------------------------- - // Implementation of JDBC 4.0's Wrapper interface - //--------------------------------------------------------------------- + @Override + public Logger getParentLogger() { + return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); + } @Override @SuppressWarnings("unchecked") @@ -143,14 +143,4 @@ public boolean isWrapperFor(Class iface) throws SQLException { return (iface.isInstance(this) || obtainTargetDataSource().isWrapperFor(iface)); } - - //--------------------------------------------------------------------- - // Implementation of JDBC 4.1's getParentLogger method - //--------------------------------------------------------------------- - - @Override - public Logger getParentLogger() { - return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); - } - } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java index cc71ba2c9d18..a0c394505852 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -61,7 +61,7 @@ public abstract class AbstractRoutingDataSource extends AbstractDataSource imple /** * Specify the map of target DataSources, with the lookup key as key. - * The mapped value can either be a corresponding {@link javax.sql.DataSource} + *

The mapped value can either be a corresponding {@link javax.sql.DataSource} * instance or a data source name String (to be resolved via a * {@link #setDataSourceLookup DataSourceLookup}). *

The key can be of arbitrary type; this class implements the @@ -213,6 +213,7 @@ public boolean isWrapperFor(Class iface) throws SQLException { return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface)); } + /** * Retrieve the current target DataSource. Determines the * {@link #determineCurrentLookupKey() current lookup key}, performs diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java index 1f8904b62bb4..6e42108c96c7 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java @@ -353,6 +353,11 @@ public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, C findInjectionMetadata(beanDefinition, beanType, beanName); } + @Override + public void resetBeanDefinition(String beanName) { + this.injectionMetadataCache.remove(beanName); + } + @Override public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); @@ -373,11 +378,6 @@ private InjectionMetadata findInjectionMetadata(RootBeanDefinition beanDefinitio return metadata; } - @Override - public void resetBeanDefinition(String beanName) { - this.injectionMetadataCache.remove(beanName); - } - @Override public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { InjectionMetadata metadata = findPersistenceMetadata(beanName, bean.getClass(), pvs); From 494d2ab727cb8b756ca175a60a19c9740aaccc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 11 Dec 2023 11:08:31 +0100 Subject: [PATCH 030/261] Remove .mailmap file See gh-31740 --- .mailmap | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 .mailmap diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 0d65c926d2d5..000000000000 --- a/.mailmap +++ /dev/null @@ -1,36 +0,0 @@ -Juergen Hoeller -Juergen Hoeller -Juergen Hoeller -Rossen Stoyanchev -Rossen Stoyanchev -Rossen Stoyanchev -Phillip Webb -Phillip Webb -Phillip Webb -Chris Beams -Chris Beams -Chris Beams -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Arjen Poutsma -Oliver Drotbohm -Oliver Drotbohm -Oliver Drotbohm -Oliver Drotbohm -Dave Syer -Dave Syer -Dave Syer -Dave Syer -Andy Clement -Andy Clement -Andy Clement -Andy Clement -Sam Brannen -Sam Brannen -Sam Brannen -Simon Basle -Simon Baslé - -Nick Williams From f54b19ff908ee092b529437f28556d6e0f782ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 11 Dec 2023 15:07:32 +0100 Subject: [PATCH 031/261] Start building against Reactor 2022.0.14 snapshots See gh-31815 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 8d0ce081ac88..ad1a12e0473f 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.10.13")) api(platform("io.netty:netty-bom:4.1.101.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.13")) + api(platform("io.projectreactor:reactor-bom:2022.0.14-SNAPSHOT")) api(platform("io.rsocket:rsocket-bom:1.1.3")) api(platform("org.apache.groovy:groovy-bom:4.0.15")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From 3a068b807bd923bde79db3f9587dba304eaed570 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 12 Dec 2023 12:32:21 +0000 Subject: [PATCH 032/261] Update link to stompjs library Closes gh-28409 --- .../web/websocket/stomp/configuration-performance.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc index aa0017e093dc..045d36398b3f 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc @@ -103,9 +103,9 @@ You can also use the WebSocket transport configuration shown earlier to configur maximum allowed size for incoming STOMP messages. In theory, a WebSocket message can be almost unlimited in size. In practice, WebSocket servers impose limits -- for example, 8K on Tomcat and 64K on Jetty. For this reason, STOMP clients -(such as the JavaScript https://github.com/JSteunou/webstomp-client[webstomp-client] -and others) split larger STOMP messages at 16K boundaries and send them as multiple -WebSocket messages, which requires the server to buffer and re-assemble. +such as https://github.com/stomp-js/stompjs[`stomp-js/stompjs`] and others split larger +STOMP messages at 16K boundaries and send them as multiple WebSocket messages, +which requires the server to buffer and re-assemble. Spring's STOMP-over-WebSocket support does this ,so applications can configure the maximum size for STOMP messages irrespective of WebSocket server-specific message From 2c97996796d724ea661d62fd7a7edef33653eb73 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 12 Dec 2023 14:01:55 +0100 Subject: [PATCH 033/261] Upgrade to Reactor 2022.0.14 Includes Groovy 4.0.16 and Mockito 5.8 Closes gh-31815 --- framework-platform/framework-platform.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index ad1a12e0473f..7ae800491c56 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,15 +11,15 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.10.13")) api(platform("io.netty:netty-bom:4.1.101.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.14-SNAPSHOT")) + api(platform("io.projectreactor:reactor-bom:2022.0.14")) api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.15")) + api(platform("org.apache.groovy:groovy-bom:4.0.16")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.eclipse.jetty:jetty-bom:11.0.18")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) api(platform("org.junit:junit-bom:5.9.3")) - api(platform("org.mockito:mockito-bom:5.7.0")) + api(platform("org.mockito:mockito-bom:5.8.0")) constraints { api("com.fasterxml:aalto-xml:1.3.2") From 707eb701dc895327f33334aeea5edb403e6eebed Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 12 Dec 2023 14:02:02 +0100 Subject: [PATCH 034/261] Polishing --- .../annotation/ImportRuntimeHints.java | 3 +-- .../aop/framework/AbstractAopProxyTests.java | 27 +++++++++---------- .../GenericApplicationContextTests.java | 22 +++++++-------- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java index bc3d4992c542..184872837729 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -44,7 +44,6 @@ * public MyService myService() { * return new MyService(); * } - * * } * *

If the configuration class above is processed, {@code MyHints} will be diff --git a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java index 9460d8c969c6..819b056254f4 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/AbstractAopProxyTests.java @@ -1086,8 +1086,8 @@ public Object invoke(MethodInvocation mi) throws Throwable { // NameReverter saved it back assertThat(it.getName()).isEqualTo(name1); assertThat(saver.names).hasSize(2); - assertThat(saver.names.get(0)).isEqualTo(name2); - assertThat(saver.names.get(1)).isEqualTo(name1); + assertThat(saver.names).element(0).isEqualTo(name2); + assertThat(saver.names).element(1).isEqualTo(name1); } @SuppressWarnings("serial") @@ -1178,7 +1178,7 @@ public void testEquals() { assertThat(i2).isEqualTo(i1); assertThat(proxyB).isEqualTo(proxyA); assertThat(proxyB.hashCode()).isEqualTo(proxyA.hashCode()); - assertThat(proxyA.equals(a)).isFalse(); + assertThat(proxyA).isNotEqualTo(a); // Equality checks were handled by the proxy assertThat(i1.getCount()).isEqualTo(0); @@ -1187,7 +1187,7 @@ public void testEquals() { // and won't think it's equal to B's NopInterceptor proxyA.absquatulate(); assertThat(i1.getCount()).isEqualTo(1); - assertThat(proxyA.equals(proxyB)).isFalse(); + assertThat(proxyA).isNotEqualTo(proxyB); } @Test @@ -1874,6 +1874,14 @@ public Class getTargetClass() { return target.getClass(); } + /** + * @see org.springframework.aop.TargetSource#isStatic() + */ + @Override + public boolean isStatic() { + return false; + } + /** * @see org.springframework.aop.TargetSource#getTarget() */ @@ -1903,19 +1911,10 @@ public void verify() { throw new RuntimeException("Expectation failed: " + gets + " gets and " + releases + " releases"); } } - - /** - * @see org.springframework.aop.TargetSource#isStatic() - */ - @Override - public boolean isStatic() { - return false; - } - } - static abstract class ExposedInvocationTestBean extends TestBean { + abstract static class ExposedInvocationTestBean extends TestBean { @Override public String getName() { diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java index dbcbb90cffc7..3c0287802e63 100644 --- a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java @@ -126,16 +126,16 @@ void accessAfterClosing() { assertThat(context.getBean(String.class)).isSameAs(context.getBean("testBean")); assertThat(context.getAutowireCapableBeanFactory().getBean(String.class)) - .isSameAs(context.getAutowireCapableBeanFactory().getBean("testBean")); + .isSameAs(context.getAutowireCapableBeanFactory().getBean("testBean")); context.close(); assertThatIllegalStateException() - .isThrownBy(() -> context.getBean(String.class)); + .isThrownBy(() -> context.getBean(String.class)); assertThatIllegalStateException() - .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean(String.class)); + .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean(String.class)); assertThatIllegalStateException() - .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean("testBean")); + .isThrownBy(() -> context.getAutowireCapableBeanFactory().getBean("testBean")); } @Test @@ -236,9 +236,9 @@ void individualBeanWithNullReturningSupplier() { assertThat(context.getBeanNamesForType(BeanB.class)).containsExactly("b"); assertThat(context.getBeanNamesForType(BeanC.class)).containsExactly("c"); assertThat(context.getBeansOfType(BeanA.class)).isEmpty(); - assertThat(context.getBeansOfType(BeanB.class).values().iterator().next()) + assertThat(context.getBeansOfType(BeanB.class).values()).element(0) .isSameAs(context.getBean(BeanB.class)); - assertThat(context.getBeansOfType(BeanC.class).values().iterator().next()) + assertThat(context.getBeansOfType(BeanC.class).values()).element(0) .isSameAs(context.getBean(BeanC.class)); } @@ -281,8 +281,8 @@ private void assertGetResourceSemantics(ResourceLoader resourceLoader, Class at index 4: ping:foo if (resourceLoader instanceof FileSystemResourceLoader && OS.WINDOWS.isCurrentOs()) { assertThatExceptionOfType(InvalidPathException.class) - .isThrownBy(() -> context.getResource(pingLocation)) - .withMessageContaining(pingLocation); + .isThrownBy(() -> context.getResource(pingLocation)) + .withMessageContaining(pingLocation); } else { resource = context.getResource(pingLocation); @@ -297,8 +297,8 @@ private void assertGetResourceSemantics(ResourceLoader resourceLoader, Class new String(bar.getByteArray(), UTF_8)) - .isEqualTo("pong:foo"); + .extracting(bar -> new String(bar.getByteArray(), UTF_8)) + .isEqualTo("pong:foo"); } @Test @@ -536,7 +536,7 @@ public BeanA(BeanB b, BeanC c) { } } - static class BeanB implements ApplicationContextAware { + static class BeanB implements ApplicationContextAware { ApplicationContext applicationContext; From 20dd585c9387c4c65762eee4802667256c2877ba Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:42:58 +0100 Subject: [PATCH 035/261] Polish MergedAnnotation tests (cherry picked from commit 952223dcf9ea07fa35e427ed76f0cc85ed8b6a80) --- .../annotation/AnnotationsScannerTests.java | 4 +- .../annotation/MergedAnnotationsTests.java | 44 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java index 792dc1f6def8..b0be3a216b57 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java @@ -197,7 +197,7 @@ void typeHierarchyStrategyOnClassWhenHasSuperclassScansSuperclass() { } @Test - void typeHierarchyStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + void typeHierarchyStrategyOnClassWhenHasSingleInterfaceScansInterfaces() { Class source = WithSingleInterface.class; assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); @@ -353,7 +353,7 @@ void typeHierarchyStrategyOnMethodWhenHasSuperclassScansSuperclass() { } @Test - void typeHierarchyStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + void typeHierarchyStrategyOnMethodWhenHasInterfaceScansInterfaces() { Method source = methodFrom(WithSingleInterface.class); assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 8ffd0eeb8939..340b0031a210 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -679,12 +679,9 @@ void getWithTypeHierarchyFromSubSubNonInheritedAnnotationInterface() { } @Test - void getWithTypeHierarchyInheritedFromInterfaceMethod() - throws NoSuchMethodException { - Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( - "handleFromInterface"); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Order.class); + void getWithTypeHierarchyInheritedFromInterfaceMethod() throws Exception { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleFromInterface"); + MergedAnnotation annotation = MergedAnnotations.from(method,SearchStrategy.TYPE_HIERARCHY).get(Order.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } @@ -1384,7 +1381,7 @@ void getDefaultValueFromAnnotationType() { } @Test - void getRepeatableDeclaredOnMethod() throws Exception { + void streamRepeatableDeclaredOnMethod() throws Exception { Method method = InterfaceWithRepeated.class.getMethod("foo"); Stream> annotations = MergedAnnotations.from( method, SearchStrategy.TYPE_HIERARCHY).stream(MyRepeatable.class); @@ -1395,7 +1392,7 @@ void getRepeatableDeclaredOnMethod() throws Exception { @Test @SuppressWarnings("deprecation") - void getRepeatableDeclaredOnClassWithAttributeAliases() { + void streamRepeatableDeclaredOnClassWithAttributeAliases() { assertThat(MergedAnnotations.from(HierarchyClass.class).stream( TestConfiguration.class)).isEmpty(); RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, @@ -1409,7 +1406,7 @@ void getRepeatableDeclaredOnClassWithAttributeAliases() { } @Test - void getRepeatableDeclaredOnClass() { + void streamRepeatableDeclaredOnClass() { Class element = MyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1417,7 +1414,7 @@ void getRepeatableDeclaredOnClass() { } @Test - void getRepeatableDeclaredOnSuperclass() { + void streamRepeatableDeclaredOnSuperclass() { Class element = SubMyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1425,7 +1422,7 @@ void getRepeatableDeclaredOnSuperclass() { } @Test - void getRepeatableDeclaredOnClassAndSuperclass() { + void streamRepeatableDeclaredOnClassAndSuperclass() { Class element = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class; String[] expectedValuesJava = { "X", "Y", "Z" }; String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; @@ -1433,7 +1430,7 @@ void getRepeatableDeclaredOnClassAndSuperclass() { } @Test - void getRepeatableDeclaredOnMultipleSuperclasses() { + void streamRepeatableDeclaredOnMultipleSuperclasses() { Class element = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class; String[] expectedValuesJava = { "X", "Y", "Z" }; String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; @@ -1441,7 +1438,7 @@ void getRepeatableDeclaredOnMultipleSuperclasses() { } @Test - void getDirectRepeatablesDeclaredOnClass() { + void streamDirectRepeatablesDeclaredOnClass() { Class element = MyRepeatableClass.class; String[] expectedValuesJava = { "A", "B", "C" }; String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; @@ -1449,7 +1446,7 @@ void getDirectRepeatablesDeclaredOnClass() { } @Test - void getDirectRepeatablesDeclaredOnSuperclass() { + void streamDirectRepeatablesDeclaredOnSuperclass() { Class element = SubMyRepeatableClass.class; String[] expectedValuesJava = {}; String[] expectedValuesSpring = {}; @@ -1476,20 +1473,17 @@ private void testExplicitRepeatables(SearchStrategy searchStrategy, Class ele MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class), AnnotationFilter.PLAIN); - assertThat(annotations.stream(MyRepeatable.class).filter( - MergedAnnotationPredicates.firstRunOf( - MergedAnnotation::getAggregateIndex)).map( - annotation -> annotation.getString( - "value"))).containsExactly(expected); + Stream values = annotations.stream(MyRepeatable.class) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .map(annotation -> annotation.getString("value")); + assertThat(values).containsExactly(expected); } private void testStandardRepeatables(SearchStrategy searchStrategy, Class element, String[] expected) { - MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy); - assertThat(annotations.stream(MyRepeatable.class).filter( - MergedAnnotationPredicates.firstRunOf( - MergedAnnotation::getAggregateIndex)).map( - annotation -> annotation.getString( - "value"))).containsExactly(expected); + Stream values = MergedAnnotations.from(element, searchStrategy).stream(MyRepeatable.class) + .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) + .map(annotation -> annotation.getString("value")); + assertThat(values).containsExactly(expected); } @Test From 1e742aae34e4d32a703720fa8326c6c439efb498 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:28:48 +0100 Subject: [PATCH 036/261] Scan annotations on method in interface hierarchy only once Prior to this commit, the AnnotationsScanner used in the MergedAnnotations infrastructure found duplicate annotations on methods within multi-level interface hierarchies. This commit addresses this issue by scanning methods at a given level in the interface hierarchy using ReflectionUtils#getDeclaredMethods instead of Class#getMethods, since the latter includes public methods declared in super-interfaces which will anyway be scanned when processing super-interfaces recursively. Closes gh-31803 (cherry picked from commit 75da9c3c474374289cc128b714126d9d644b69a9) --- .../core/annotation/AnnotationsScanner.java | 7 ++-- .../annotation/AnnotationsScannerTests.java | 35 ++++++++++++++++++- .../annotation/MergedAnnotationsTests.java | 12 +++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index 61fea0685226..e6a8626e05ad 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -336,11 +336,10 @@ private static Method[] getBaseTypeMethods(C context, Class baseType) { Method[] methods = baseTypeMethodsCache.get(baseType); if (methods == null) { - boolean isInterface = baseType.isInterface(); - methods = isInterface ? baseType.getMethods() : ReflectionUtils.getDeclaredMethods(baseType); + methods = ReflectionUtils.getDeclaredMethods(baseType); int cleared = 0; for (int i = 0; i < methods.length; i++) { - if ((!isInterface && Modifier.isPrivate(methods[i].getModifiers())) || + if (Modifier.isPrivate(methods[i].getModifiers()) || hasPlainJavaAnnotationsOnly(methods[i]) || getDeclaredAnnotations(methods[i], false).length == 0) { methods[i] = null; diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java index b0be3a216b57..3fa06ca8e780 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -357,6 +357,15 @@ void typeHierarchyStrategyOnMethodWhenHasInterfaceScansInterfaces() { Method source = methodFrom(WithSingleInterface.class); assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly( "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + + source = methodFrom(Hello1Impl.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("1:TestAnnotation1"); + } + + @Test // gh-31803 + void typeHierarchyStrategyOnMethodWhenHasInterfaceHierarchyScansInterfacesOnlyOnce() { + Method source = methodFrom(Hello2Impl.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly("1:TestAnnotation1"); } @Test @@ -691,6 +700,30 @@ public void method() { } } + interface Hello1 { + + @TestAnnotation1 + void method(); + } + + interface Hello2 extends Hello1 { + } + + static class Hello1Impl implements Hello1 { + + @Override + public void method() { + } + } + + static class Hello2Impl implements Hello2 { + + @Override + public void method() { + } + } + + @TestAnnotation2 @TestInheritedAnnotation2 static class HierarchySuperclass extends HierarchySuperSuperclass { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 340b0031a210..f73654666c91 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -40,6 +40,8 @@ import org.junit.jupiter.api.Test; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationsScannerTests.Hello2Impl; +import org.springframework.core.annotation.AnnotationsScannerTests.TestAnnotation1; import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.core.annotation.MergedAnnotations.Search; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; @@ -686,6 +688,16 @@ void getWithTypeHierarchyInheritedFromInterfaceMethod() throws Exception { assertThat(annotation.getAggregateIndex()).isEqualTo(1); } + @Test // gh-31803 + void streamWithTypeHierarchyInheritedFromSuperInterfaceMethod() throws Exception { + Method method = Hello2Impl.class.getMethod("method"); + long count = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY) + .from(method) + .stream(TestAnnotation1.class) + .count(); + assertThat(count).isEqualTo(1); + } + @Test void getWithTypeHierarchyInheritedFromAbstractMethod() throws NoSuchMethodException { Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handle"); From db52c77cca58689f9305611486454f6ab56079ca Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 11 Dec 2023 10:24:39 +0000 Subject: [PATCH 037/261] Minor updates in HandlerMappingIntrospector Required by Spring Security to complete work on https://github.com/spring-projects/spring-security/issues/14128 The setCache and resetCache methods used from createCacheFilter are now public. Generally they don't need to be used outside of the Filter if only making checks against the current request. Spring Security, however, makes additional checks against requests with alternative paths. --- .../handler/HandlerMappingIntrospector.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 31f76869bc7f..30a2d6755b6c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -191,8 +191,12 @@ public List getHandlerMappings() { public Filter createCacheFilter() { return (request, response, chain) -> { CachedResult previous = setCache((HttpServletRequest) request); - chain.doFilter(request, response); - resetCache(request, previous); + try { + chain.doFilter(request, response); + } + finally { + resetCache(request, previous); + } }; } @@ -206,7 +210,7 @@ public Filter createCacheFilter() { * @since 6.0.14 */ @Nullable - private CachedResult setCache(HttpServletRequest request) { + public CachedResult setCache(HttpServletRequest request) { CachedResult previous = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE); if (previous == null || !previous.matches(request)) { HttpServletRequest wrapped = new AttributesPreservingRequest(request); @@ -245,7 +249,7 @@ private CachedResult setCache(HttpServletRequest request) { * a filter after delegating to the rest of the chain. * @since 6.0.14 */ - private void resetCache(ServletRequest request, @Nullable CachedResult cachedResult) { + public void resetCache(ServletRequest request, @Nullable CachedResult cachedResult) { request.setAttribute(CACHED_RESULT_ATTRIBUTE, cachedResult); } @@ -363,7 +367,7 @@ private T doWithHandlerMapping( * @since 6.0.14 */ @SuppressWarnings("serial") - private static final class CachedResult { + public static final class CachedResult { private final DispatcherType dispatcherType; From 76bc9cf325cd6fcea838453bedfde17182980db0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Dec 2023 14:33:13 +0100 Subject: [PATCH 038/261] Prepare method overrides when bean class gets resolved See gh-31826 See gh-31828 (cherry picked from commit cd64e6676c67ebabb242be6de0eb3cdee9ba6dc2) --- .../AbstractAutowireCapableBeanFactory.java | 16 +++++++--------- .../factory/support/AbstractBeanFactory.java | 10 +++++++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 7de716b44c95..cdf9307e3d79 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -493,15 +493,13 @@ protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable O if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { mbdToUse = new RootBeanDefinition(mbd); mbdToUse.setBeanClass(resolvedClass); - } - - // Prepare method overrides. - try { - mbdToUse.prepareMethodOverrides(); - } - catch (BeanDefinitionValidationException ex) { - throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), - beanName, "Validation of method overrides failed", ex); + try { + mbdToUse.prepareMethodOverrides(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } } try { 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 57b9b8dffad8..6cae5bc70599 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 @@ -1497,7 +1497,11 @@ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Cla if (mbd.hasBeanClass()) { return mbd.getBeanClass(); } - return doResolveBeanClass(mbd, typesToMatch); + Class beanClass = doResolveBeanClass(mbd, typesToMatch); + if (mbd.hasBeanClass()) { + mbd.prepareMethodOverrides(); + } + return beanClass; } catch (ClassNotFoundException ex) { throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), ex); @@ -1505,6 +1509,10 @@ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Cla catch (LinkageError err) { throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), err); } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbd.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } } @Nullable From 67e03105b587a3fb3cc7b776d9e685f0837a6b68 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:00:44 +0100 Subject: [PATCH 039/261] Introduce test for XML replaced-method element without explicit arg-type This commit introduces an integration test for the regression fixed in the previous commit (76bc9cf325). See gh-31826 Closes gh-31828 (cherry picked from commit 8d4deca2a658d8ea456a18ecce8a7418373030c4) --- .../factory/xml/XmlBeanFactoryTests.java | 33 +++++++++++++++++++ ...mlBeanFactoryTests-delegationOverrides.xml | 9 +++++ 2 files changed, 42 insertions(+) diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java index d42775bd2ad8..722e6010aad6 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/XmlBeanFactoryTests.java @@ -22,8 +22,13 @@ import java.io.InputStreamReader; import java.io.StringWriter; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; @@ -56,6 +61,8 @@ import org.springframework.beans.testfixture.beans.IndexedTestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.factory.DummyFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.UrlResource; @@ -1336,6 +1343,15 @@ void replaceMethodOverrideWithSetterInjection() { assertThat(dos.lastArg).isEqualTo(s2); } + @Test // gh-31826 + void replaceNonOverloadedInterfaceMethodWithoutSpecifyingExplicitArgTypes() { + try (ConfigurableApplicationContext context = + new ClassPathXmlApplicationContext(DELEGATION_OVERRIDES_CONTEXT.getPath())) { + EchoService echoService = context.getBean(EchoService.class); + assertThat(echoService.echo("foo", "bar")).containsExactly("bar", "foo"); + } + } + @Test void lookupOverrideOneMethodWithConstructorInjection() { DefaultListableBeanFactory xbf = new DefaultListableBeanFactory(); @@ -1891,3 +1907,20 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw } } + +interface EchoService { + + String[] echo(Object... objects); +} + +class ReverseArrayMethodReplacer implements MethodReplacer { + + @Override + public Object reimplement(Object obj, Method method, Object[] args) { + List list = Arrays.stream((Object[]) args[0]) + .map(Object::toString) + .collect(Collectors.toCollection(ArrayList::new)); + Collections.reverse(list); + return list.toArray(String[]::new); + } +} diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml index 175408a2cb39..208fdf7b8f3c 100644 --- a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml @@ -36,12 +36,21 @@ + + + + + + From ccaecab500d603443bc2bb8fe515e225bc80f9f1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 14 Dec 2023 08:32:08 +0100 Subject: [PATCH 040/261] Polishing --- .../aot/AutowiredFieldValueResolver.java | 10 ++--- .../factory/support/AbstractBeanFactory.java | 2 +- .../support/AbstractApplicationContext.java | 2 +- ...mlBeanFactoryTests-delegationOverrides.xml | 38 +++++-------------- 4 files changed, 16 insertions(+), 36 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java index 12cfc76b0833..1c5f68fe902f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java @@ -191,16 +191,14 @@ private Object resolveValue(RegisteredBean registeredBean, Field field) { return value; } catch (BeansException ex) { - throw new UnsatisfiedDependencyException(null, beanName, - new InjectionPoint(field), ex); + throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex); } } private Field getField(RegisteredBean registeredBean) { - Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), - this.fieldName); - Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " - + registeredBean.getBeanClass().getName()); + Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), this.fieldName); + Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " + + registeredBean.getBeanClass().getName()); return field; } 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 6cae5bc70599..5d84e258a141 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 @@ -731,7 +731,7 @@ public String[] getAliases(String name) { aliases.add(fullBeanName); } String[] retrievedAliases = super.getAliases(beanName); - String prefix = factoryPrefix ? FACTORY_BEAN_PREFIX : ""; + String prefix = (factoryPrefix ? FACTORY_BEAN_PREFIX : ""); for (String retrievedAlias : retrievedAliases) { String alias = prefix + retrievedAlias; if (!alias.equals(name)) { 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 36b2c76f8387..e87311ecbad2 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 @@ -203,7 +203,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader /** Flag that indicates whether this context has been closed already. */ private final AtomicBoolean closed = new AtomicBoolean(); - /** Synchronization monitor for the "refresh" and "destroy". */ + /** Synchronization monitor for "refresh" and "close". */ private final Object startupShutdownMonitor = new Object(); /** Reference to the JVM shutdown hook, if registered. */ diff --git a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml index 208fdf7b8f3c..d1a875359d3c 100644 --- a/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml +++ b/spring-context/src/test/resources/org/springframework/beans/factory/xml/XmlBeanFactoryTests-delegationOverrides.xml @@ -3,9 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> - @@ -27,48 +24,34 @@ - + - - + - + - - - + - + - + - + + String - - + Jenny 30 @@ -77,8 +60,7 @@ - + Simple bean, without any collections. From bafff5079f8f141f8f8b02dcc0c17107a5298ad7 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 14 Dec 2023 08:52:04 +0000 Subject: [PATCH 041/261] Next development version (v6.0.16-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 16e9ab72d674..1f188bcc477b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.15-SNAPSHOT +version=6.0.16-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From b272f4e6153afed10b6eb1dab939b2e142a7a96a Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 18 Dec 2023 11:43:18 +0100 Subject: [PATCH 042/261] Correctly set capacity of remainder in DefaultDataBuffer::split This commit ensures that the capacity of the remainder buffer after a split operation is set directly on the field. Calling capacity(int) caused a new buffer to be allocated. See gh-31848 Closes gh-31859 --- .../org/springframework/core/io/buffer/DefaultDataBuffer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index def3ccf39649..4456f975bd9e 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -371,7 +371,7 @@ public DataBuffer split(int index) { .slice(); this.writePosition = Math.max(this.writePosition, index) - index; this.readPosition = Math.max(this.readPosition, index) - index; - capacity(this.byteBuffer.capacity()); + this.capacity = this.byteBuffer.capacity(); return result; } From 033c8df53f097363564b2b3a7d57fc98eecd8b1a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 22 Dec 2023 12:54:16 +0100 Subject: [PATCH 043/261] Polishing --- .../aop/framework/AdvisedSupport.java | 34 +++++++++++-------- .../factory/support/ConstructorResolver.java | 6 +--- .../cache/caffeine/CaffeineCacheManager.java | 2 +- .../interceptor/CacheOperationInvoker.java | 4 +-- .../scheduling/TaskScheduler.java | 2 +- .../core/SerializableTypeWrapper.java | 2 +- .../spel/StandardTypeComparatorTests.java | 7 ++-- .../SimpleClientHttpRequestFactoryTests.java | 5 +-- .../reactive/ClientHttpConnectorTests.java | 4 +-- 9 files changed, 33 insertions(+), 33 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java index 41c5b1d21c1d..477c85c88223 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -102,6 +102,12 @@ public class AdvisedSupport extends ProxyConfig implements Advised { */ private List advisors = new ArrayList<>(); + /** + * List of minimal {@link AdvisorKeyEntry} instances, + * to be assigned to the {@link #advisors} field on reduction. + * @since 6.0.10 + * @see #reduceToAdvisorKey + */ private List advisorKey = this.advisors; @@ -557,18 +563,6 @@ Object getAdvisorKey() { } - //--------------------------------------------------------------------- - // Serialization support - //--------------------------------------------------------------------- - - private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { - // Rely on default serialization; just initialize state after deserialization. - ois.defaultReadObject(); - - // Initialize transient fields. - this.methodCache = new ConcurrentHashMap<>(32); - } - @Override public String toProxyConfigString() { return toString(); @@ -590,6 +584,19 @@ public String toString() { } + //--------------------------------------------------------------------- + // Serialization support + //--------------------------------------------------------------------- + + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { + // Rely on default serialization; just initialize state after deserialization. + ois.defaultReadObject(); + + // Initialize transient fields. + this.methodCache = new ConcurrentHashMap<>(32); + } + + /** * Simple wrapper class around a Method. Used as the key when * caching methods, for efficient equals and hashCode comparisons. @@ -639,7 +646,7 @@ public int compareTo(MethodCacheKey other) { * @see #getConfigurationOnlyCopy() * @see #getAdvisorKey() */ - private static class AdvisorKeyEntry implements Advisor { + private static final class AdvisorKeyEntry implements Advisor { private final Class adviceType; @@ -649,7 +656,6 @@ private static class AdvisorKeyEntry implements Advisor { @Nullable private final String methodMatcherKey; - public AdvisorKeyEntry(Advisor advisor) { this.adviceType = advisor.getAdvice().getClass(); if (advisor instanceof PointcutAdvisor pointcutAdvisor) { 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 6af615a32049..df91c0ca0895 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 @@ -120,11 +120,7 @@ public ConstructorResolver(AbstractAutowireCapableBeanFactory beanFactory) { /** * "autowire constructor" (with constructor arguments by type) behavior. - * Also applied if explicit constructor argument values are specified, - * matching all remaining arguments with beans from the bean factory. - *

This corresponds to constructor injection: In this mode, a Spring - * bean factory is able to host components that expect constructor-based - * dependency resolution. + * Also applied if explicit constructor argument values are specified. * @param beanName the name of the bean * @param mbd the merged bean definition for the bean * @param chosenCtors chosen candidate constructors (or {@code null} if none) diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java index e80cb747f396..e03d19c31412 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -236,7 +236,7 @@ protected Cache adaptCaffeineCache(String name, com.github.benmanes.caffeine.cac * Build a common {@link CaffeineCache} instance for the specified cache name, * using the common Caffeine configuration specified on this cache manager. *

Delegates to {@link #adaptCaffeineCache} as the adaptation method to - * Spring's cache abstraction (allowing for centralized decoration etc), + * Spring's cache abstraction (allowing for centralized decoration etc.), * passing in a freshly built native Caffeine Cache instance. * @param name the name of the cache * @return the Spring CaffeineCache adapter (or a decorator thereof) diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java index cfaab08137bd..e37736480e51 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -22,7 +22,7 @@ * Abstract the invocation of a cache operation. * *

Does not provide a way to transmit checked exceptions but - * provide a special exception that should be used to wrap any + * provides a special exception that should be used to wrap any * exception that was thrown by the underlying invocation. * Callers are expected to handle this issue type specifically. * diff --git a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java index 00cb01282cc3..a99d6069201e 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java @@ -70,7 +70,7 @@ default Clock getClock() { * wrapping a cron expression * @return a {@link ScheduledFuture} representing pending completion of the task, * or {@code null} if the given Trigger object never fires (i.e. returns - * {@code null} from {@link Trigger#nextExecutionTime}) + * {@code null} from {@link Trigger#nextExecution}) * @throws org.springframework.core.task.TaskRejectedException if the given task was not accepted * for internal reasons (e.g. a pool overload handling policy or a pool shutdown in progress) * @see org.springframework.scheduling.support.CronTrigger diff --git a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java index 01e5a42b5ec7..44f5719ad74b 100644 --- a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java +++ b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java @@ -159,7 +159,7 @@ interface TypeProvider extends Serializable { /** * Return the source of the type, or {@code null} if not known. - *

The default implementations returns {@code null}. + *

The default implementation returns {@code null}. */ @Nullable default Object getSource() { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/StandardTypeComparatorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/StandardTypeComparatorTests.java index c962943c4c29..48975c081974 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/StandardTypeComparatorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/StandardTypeComparatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -23,7 +23,6 @@ import org.springframework.expression.EvaluationException; import org.springframework.expression.TypeComparator; import org.springframework.expression.spel.support.StandardTypeComparator; -import org.springframework.lang.NonNull; import static org.assertj.core.api.Assertions.assertThat; @@ -134,6 +133,7 @@ public void shouldUseCustomComparator() { assertThat(comparator.compare(t2, t1)).isPositive(); } + static class ComparableType implements Comparable { private final int id; @@ -143,10 +143,9 @@ public ComparableType(int id) { } @Override - public int compareTo(@NonNull ComparableType other) { + public int compareTo(ComparableType other) { return this.id - other.id; } - } } diff --git a/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java index fc4755db0161..be08f3af12f4 100644 --- a/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/SimpleClientHttpRequestFactoryTests.java @@ -32,14 +32,15 @@ */ public class SimpleClientHttpRequestFactoryTests { - - @Test // SPR-13225 + @Test // SPR-13225 public void headerWithNullValue() { HttpURLConnection urlConnection = mock(); given(urlConnection.getRequestMethod()).willReturn("GET"); + HttpHeaders headers = new HttpHeaders(); headers.set("foo", null); SimpleBufferingClientHttpRequest.addHeaders(urlConnection, headers); + verify(urlConnection, times(1)).addRequestProperty("foo", ""); } diff --git a/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java b/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java index a49b0816809b..827ba44dbd34 100644 --- a/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -51,7 +51,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ReactiveHttpOutputMessage; -import org.springframework.lang.NonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; @@ -173,7 +172,6 @@ void cancelResponseBody(ClientHttpConnector connector) { .verify(); } - @NonNull private Buffer randomBody(int size) { Buffer responseBody = new Buffer(); Random rnd = new Random(); From c9163b77df52c5e1def643d9628965f4295dfc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 22 Dec 2023 15:19:01 +0100 Subject: [PATCH 044/261] Add support for `@Async` Kotlin function returning `Unit?` Closes gh-31891 --- .../AsyncExecutionAspectSupport.java | 3 +- ...ionAsyncExecutionInterceptorKotlinTests.kt | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 spring-context/src/test/kotlin/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorKotlinTests.kt diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java index 5d35e87994b5..ff72b90056b6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java @@ -58,6 +58,7 @@ * @author Juergen Hoeller * @author Stephane Nicoll * @author He Bo + * @author Sebastien Deleuze * @since 3.1.2 */ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { @@ -290,7 +291,7 @@ else if (org.springframework.util.concurrent.ListenableFuture.class.isAssignable else if (Future.class.isAssignableFrom(returnType)) { return executor.submit(task); } - else if (void.class == returnType) { + else if (void.class == returnType || "kotlin.Unit".equals(returnType.getName())) { executor.submit(task); return null; } diff --git a/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorKotlinTests.kt b/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorKotlinTests.kt new file mode 100644 index 000000000000..86548ef0d994 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptorKotlinTests.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2023 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.scheduling.annotation + +import org.aopalliance.intercept.MethodInvocation +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given +import org.mockito.Mockito + +/** + * Kotlin tests for [AnnotationAsyncExecutionInterceptor]. + * + * @author Sebastien Deleuze + */ +class AnnotationAsyncExecutionInterceptorKotlinTests { + + @Test + fun nullableUnitReturnValue() { + val interceptor = AnnotationAsyncExecutionInterceptor(null) + + class C { @Async fun nullableUnit(): Unit? = null } + val invocation = Mockito.mock() + given(invocation.method).willReturn(C::class.java.getDeclaredMethod("nullableUnit")) + + Assertions.assertThat(interceptor.invoke(invocation)).isNull() + } + +} From 9d13ea290f54fc7d4616418bc9f91db8cf6b664e Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 22 Dec 2023 17:27:06 +0100 Subject: [PATCH 045/261] Reject invalid forwarded requests in ForwardedHeaderFilter Prior to this commit, the `ForwardedHeaderFilter` and the forwarded header utils would throw `IllegalArgumentException` and `IllegalStateException` when request headers are invalid and cannot be parsed for Forwarded handling. This commit aligns the behavior with the WebFlux counterpart by rejecting such requests with HTTP 400 responses directly. Fixes gh-31894 --- .../web/filter/ForwardedHeaderFilter.java | 36 ++++++++++++---- .../filter/ForwardedHeaderFilterTests.java | 41 +++++++++++++++---- 2 files changed, 63 insertions(+), 14 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 abb2b469b002..5c631fcd8dc0 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 @@ -30,6 +30,8 @@ import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpStatus; import org.springframework.http.server.ServerHttpRequest; @@ -64,11 +66,14 @@ * @author Rossen Stoyanchev * @author Eddú Meléndez * @author Rob Winch + * @author Brian Clozel * @since 4.3 * @see https://tools.ietf.org/html/rfc7239 */ public class ForwardedHeaderFilter extends OncePerRequestFilter { + private static final Log logger = LogFactory.getLog(ForwardedHeaderFilter.class); + private static final Set FORWARDED_HEADER_NAMES = Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(10, Locale.ENGLISH)); @@ -143,17 +148,34 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(wrappedRequest, response); } else { - HttpServletRequest wrappedRequest = - new ForwardedHeaderExtractingRequest(request); - - HttpServletResponse wrappedResponse = this.relativeRedirects ? - RelativeRedirectResponseWrapper.wrapIfNecessary(response, HttpStatus.SEE_OTHER) : - new ForwardedHeaderExtractingResponse(response, wrappedRequest); - + HttpServletRequest wrappedRequest = null; + HttpServletResponse wrappedResponse = null; + try { + wrappedRequest = new ForwardedHeaderExtractingRequest(request); + wrappedResponse = this.relativeRedirects ? + RelativeRedirectResponseWrapper.wrapIfNecessary(response, HttpStatus.SEE_OTHER) : + new ForwardedHeaderExtractingResponse(response, wrappedRequest); + } + catch (Throwable ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to apply forwarded headers to " + formatRequest(request), ex); + } + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } filterChain.doFilter(wrappedRequest, wrappedResponse); } } + /** + * Format the request for logging purposes including HTTP method and URL. + * @param request the request to format + * @return the String to display, never empty or {@code null} + */ + protected String formatRequest(HttpServletRequest request) { + return "HTTP " + request.getMethod() + " \"" + request.getRequestURI() + "\""; + } + @Override protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { diff --git a/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java index bf4a4c722ff2..2fae32527b85 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java @@ -32,12 +32,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.HttpStatus; import org.springframework.web.testfixture.servlet.MockFilterChain; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.mock; /** @@ -46,6 +46,7 @@ * @author Rossen Stoyanchev * @author Eddú Meléndez * @author Rob Winch + * @author Brian Clozel */ public class ForwardedHeaderFilterTests { @@ -208,6 +209,38 @@ public void forwardedRequestWithServletForward() throws Exception { assertThat(actual.getRequestURL().toString()).isEqualTo("https://www.mycompany.example/bar"); } + @Nested // gh-31842 + class InvalidRequests { + + @Test + void shouldRejectInvalidForwardedForIpv4() throws Exception { + request.addHeader(FORWARDED, "for=127.0.0.1:"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void shouldRejectInvalidForwardedForIpv6() throws Exception { + request.addHeader(FORWARDED, "for=\"2a02:918:175:ab60:45ee:c12c:dac1:808b\""); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void shouldRejectInvalidForwardedPort() throws Exception { + request.addHeader(X_FORWARDED_PORT, "invalid"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(request, response, filterChain); + assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + } + @Nested class ForwardedPrefix { @@ -474,12 +507,6 @@ public void forwardedForMultipleIdentifiers() throws Exception { assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); } - @Test // gh-26748 - public void forwardedForInvalidIpV6Address() { - request.addHeader(FORWARDED, "for=\"2a02:918:175:ab60:45ee:c12c:dac1:808b\""); - assertThatIllegalArgumentException().isThrownBy( - ForwardedHeaderFilterTests.this::filterAndGetWrappedRequest); - } } @Nested From b1b6b544a2b374d3f84ffff73bdca119251de42c Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Thu, 28 Dec 2023 14:50:00 +0800 Subject: [PATCH 046/261] Add missing @Test See gh-31914 --- .../java/org/springframework/mail/SimpleMailMessageTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java b/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java index 5312152ce4ce..bac90ab18171 100644 --- a/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java +++ b/spring-context-support/src/test/java/org/springframework/mail/SimpleMailMessageTests.java @@ -121,6 +121,7 @@ public final void testHashCode() { assertThat(message2.hashCode()).isEqualTo(message1.hashCode()); } + @Test public final void testEqualsObject() { SimpleMailMessage message1; SimpleMailMessage message2; From 198cf063fdc9d1c47d3c5bb3b3cc24290c047a91 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 30 Dec 2023 11:45:34 +0100 Subject: [PATCH 047/261] Polishing --- .../ROOT/pages/core/aop-api/targetsource.adoc | 2 +- .../ROOT/pages/core/beans/factory-scopes.adoc | 3 --- .../modules/ROOT/pages/integration/email.adoc | 2 +- .../modules/ROOT/pages/web/webmvc/filters.adoc | 5 +++-- .../support/AbstractAutowireCapableBeanFactory.java | 10 +++++----- .../annotation/PropertySourceAnnotationTests.java | 12 ++++++------ .../core/io/support/PropertySourceDescriptor.java | 4 ++-- .../interceptor/TransactionAspectSupport.java | 4 ++-- .../server/ServletServerHttpAsyncRequestControl.java | 12 ++++++------ .../server/reactive/ServletHttpHandlerAdapter.java | 12 ++++++------ .../async/StandardServletAsyncWebRequest.java | 8 ++++---- 11 files changed, 36 insertions(+), 38 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc index fdb41e7b4d18..70d19d9fa74d 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc @@ -168,7 +168,7 @@ Kotlin:: ====== NOTE: Pooling stateless service objects is not usually necessary. We do not believe it should -be the default choice, as most stateless objects are naturally thread safe, and instance +be the default choice, as most stateless objects are naturally thread-safe, and instance pooling is problematic if resources are cached. Simpler pooling is available by using auto-proxying. You can set the `TargetSource` implementations diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc index 8243d755d1e1..0b0e00a52d79 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-scopes.adoc @@ -324,7 +324,6 @@ Kotlin:: - [[beans-factory-scopes-application]] === Application Scope @@ -374,7 +373,6 @@ Kotlin:: - [[beans-factory-scopes-websocket]] === WebSocket Scope @@ -384,7 +382,6 @@ xref:web/websocket/stomp/scope.adoc[WebSocket scope] for more details. - [[beans-factory-scopes-other-injection]] === Scoped Beans as Dependencies diff --git a/framework-docs/modules/ROOT/pages/integration/email.adoc b/framework-docs/modules/ROOT/pages/integration/email.adoc index 610fda7c8797..71c1db30523b 100644 --- a/framework-docs/modules/ROOT/pages/integration/email.adoc +++ b/framework-docs/modules/ROOT/pages/integration/email.adoc @@ -85,7 +85,7 @@ email when someone places an order: // Call the collaborators to persist the order... - // Create a thread safe "copy" of the template message and customize it + // Create a thread-safe "copy" of the template message and customize it SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage); msg.setTo(order.getCustomer().getEmailAddress()); msg.setText( diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc index 8851bc74c24a..781d5a1743ca 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc @@ -70,8 +70,9 @@ it does the same, but it also compares the computed value against the `If-None-M request header and, if the two are equal, returns a 304 (NOT_MODIFIED). This strategy saves network bandwidth but not CPU, as the full response must be computed for each request. -State-changing HTTP methods and other HTTP conditional request headers such as `If-Match` and `If-Unmodified-Since` are outside the scope of this filter. -Other strategies at the controller level can avoid the computation and have a broader support for HTTP conditional requests. +State-changing HTTP methods and other HTTP conditional request headers such as `If-Match` and +`If-Unmodified-Since` are outside the scope of this filter. Other strategies at the controller level +can avoid the computation and have a broader support for HTTP conditional requests. See xref:web/webmvc/mvc-caching.adoc[HTTP Caching]. This filter has a `writeWeakETag` parameter that configures the filter to write weak ETags diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index cdf9307e3d79..b14b9796d033 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -92,10 +92,10 @@ * Supports autowiring constructors, properties by name, and properties by type. * *

The main template method to be implemented by subclasses is - * {@link #resolveDependency(DependencyDescriptor, String, Set, TypeConverter)}, - * used for autowiring by type. In case of a factory which is capable of searching - * its bean definitions, matching beans will typically be implemented through such - * a search. For other factory styles, simplified matching algorithms can be implemented. + * {@link #resolveDependency(DependencyDescriptor, String, Set, TypeConverter)}, used for + * autowiring. In case of a {@link org.springframework.beans.factory.ListableBeanFactory} + * which is capable of searching its bean definitions, matching beans will typically be + * implemented through such a search. Otherwise, simplified matching can be implemented. * *

Note that this class does not assume or implement bean definition * registry capabilities. See {@link DefaultListableBeanFactory} for an implementation @@ -650,7 +650,7 @@ protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Clas // Apply SmartInstantiationAwareBeanPostProcessors to predict the // eventual type after a before-instantiation shortcut. if (targetType != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { - boolean matchingOnlyFactoryBean = typesToMatch.length == 1 && typesToMatch[0] == FactoryBean.class; + boolean matchingOnlyFactoryBean = (typesToMatch.length == 1 && typesToMatch[0] == FactoryBean.class); for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { Class predicted = bp.predictBeanType(targetType, beanName); if (predicted != null && diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java index a81a074b2471..d2ad48177814 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -130,8 +130,8 @@ void withCustomFactoryAsMeta() { @Test void withUnresolvablePlaceholder() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class)) - .withCauseInstanceOf(IllegalArgumentException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithUnresolvablePlaceholder.class)) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -162,8 +162,8 @@ void withResolvablePlaceholderAndFactoryBean() { @Test void withEmptyResourceLocations() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithEmptyResourceLocations.class)) - .withCauseInstanceOf(IllegalArgumentException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithEmptyResourceLocations.class)) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -245,8 +245,8 @@ void withNamedPropertySources() { @Test void withMissingPropertySource() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class)) - .withCauseInstanceOf(FileNotFoundException.class); + .isThrownBy(() -> new AnnotationConfigApplicationContext(ConfigWithMissingPropertySource.class)) + .withCauseInstanceOf(FileNotFoundException.class); } @Test diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java index 254a1d49d68c..c138de880488 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java @@ -24,6 +24,8 @@ /** * Descriptor for a {@link org.springframework.core.env.PropertySource PropertySource}. * + * @author Stephane Nicoll + * @since 6.0 * @param locations the locations to consider * @param ignoreResourceNotFound whether a failure to find a property resource * should be ignored @@ -31,8 +33,6 @@ * @param propertySourceFactory the type of {@link PropertySourceFactory} to use, * or {@code null} to use the default * @param encoding the encoding, or {@code null} to use the default encoding - * @author Stephane Nicoll - * @since 6.0 * @see org.springframework.core.env.PropertySource * @see org.springframework.context.annotation.PropertySource */ 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 1ce0313d9770..b800fb6ca629 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 @@ -358,8 +358,8 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class targe (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: " + - method.getReturnType()); + throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type [" + + method.getReturnType() + "] with specified transaction manager: " + tm); } return new ReactiveTransactionSupport(adapter); }); diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java index db87ffb8e4ff..1b50f301021a 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpAsyncRequestControl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -115,13 +115,11 @@ public void complete() { // --------------------------------------------------------------------- @Override - public void onComplete(AsyncEvent event) throws IOException { - this.asyncContext = null; - this.asyncCompleted.set(true); + public void onStartAsync(AsyncEvent event) throws IOException { } @Override - public void onStartAsync(AsyncEvent event) throws IOException { + public void onTimeout(AsyncEvent event) throws IOException { } @Override @@ -129,7 +127,9 @@ public void onError(AsyncEvent event) throws IOException { } @Override - public void onTimeout(AsyncEvent event) throws IOException { + public void onComplete(AsyncEvent event) throws IOException { + this.asyncContext = null; + this.asyncCompleted.set(true); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 456f85e4b256..500d644e2f12 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -275,6 +275,11 @@ public HttpHandlerAsyncListener(AsyncListener requestAsyncListener, AsyncListene this.logPrefix = logPrefix; } + @Override + public void onStartAsync(AsyncEvent event) { + // no-op + } + @Override public void onTimeout(AsyncEvent event) { // Should never happen since we call asyncContext.setTimeout(-1) @@ -341,11 +346,6 @@ private void handleTimeoutOrError(AsyncEvent event) { } }); } - - @Override - public void onStartAsync(AsyncEvent event) { - // no-op - } } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index bfa7513d5372..eb46ccb64790 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -145,13 +145,13 @@ public void onStartAsync(AsyncEvent event) throws IOException { } @Override - public void onError(AsyncEvent event) throws IOException { - this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable())); + public void onTimeout(AsyncEvent event) throws IOException { + this.timeoutHandlers.forEach(Runnable::run); } @Override - public void onTimeout(AsyncEvent event) throws IOException { - this.timeoutHandlers.forEach(Runnable::run); + public void onError(AsyncEvent event) throws IOException { + this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable())); } @Override From 56c63c779f84e5e6e6aabafa7c1cdbb4d95ef45f Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 4 Jan 2024 16:44:00 +0100 Subject: [PATCH 048/261] Fix HandlerMappingIntrospector uri matching Prior to this commit, the `HandlerMappingIntrospector` would comparea request with a cached request by using `String#matches` on their String URI. This could lead to `PatternSyntaxException` exceptions at runtime if the request URI contained pattern characters. This commit fixes this typo to use `String#equals` instead. Fixes gh-31946 --- .../servlet/handler/HandlerMappingIntrospector.java | 4 ++-- .../handler/HandlerMappingIntrospectorTests.java | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java index 30a2d6755b6c..b04b1c27dd5f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -399,7 +399,7 @@ private CachedResult(HttpServletRequest request, public boolean matches(HttpServletRequest request) { return (this.dispatcherType.equals(request.getDispatcherType()) && - this.requestURI.matches(request.getRequestURI())); + this.requestURI.equals(request.getRequestURI())); } @Nullable diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java index 8037a5cad579..4d8a25384798 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -204,15 +204,16 @@ void getCorsConfigurationActual() { assertThat(corsConfig.getAllowedMethods()).isEqualTo(Collections.singletonList("POST")); } - @Test - void cacheFilter() throws Exception { + @ParameterizedTest + @ValueSource(strings = {"/test", "/resource/1234****"}) // gh-31937 + void cacheFilter(String uri) throws Exception { CorsConfiguration corsConfig = new CorsConfiguration(); TestMatchableHandlerMapping mapping = new TestMatchableHandlerMapping(); - mapping.registerHandler("/test", new TestHandler(corsConfig)); + mapping.registerHandler("/*", new TestHandler(corsConfig)); HandlerMappingIntrospector introspector = initIntrospector(mapping); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); MockHttpServletResponse response = new MockHttpServletResponse(); MockFilterChain filterChain = new MockFilterChain( From 1b8baffbd7229cf47229b3edd7b09a6f7e57f471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 5 Jan 2024 09:41:16 +0100 Subject: [PATCH 049/261] Upgrade CI to Ubuntu Jammy 20231211.1 --- ci/images/ci-image/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index e8ff8af88b74..d541b6d92d6f 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:jammy-20231004 +FROM ubuntu:jammy-20231211.1 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh From d074f660a1b59d25442adad6e94c8f6c96b3b2f2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 5 Jan 2024 10:19:03 +0100 Subject: [PATCH 050/261] Default time zone resolution from scheduler-wide Clock Closes gh-31948 --- .../scheduling/annotation/Scheduled.java | 6 ++-- .../ScheduledAnnotationBeanPostProcessor.java | 11 +++---- .../scheduling/support/CronExpression.java | 4 +-- .../scheduling/support/CronTrigger.java | 31 ++++++++++--------- .../scheduling/support/CronTriggerTests.java | 7 ++--- 5 files changed, 30 insertions(+), 29 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java index 41e88d3f50bf..170560885eff 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/Scheduled.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -102,9 +102,9 @@ /** * A time zone for which the cron expression will be resolved. By default, this - * attribute is the empty String (i.e. the server's local time zone will be used). + * attribute is the empty String (i.e. the scheduler's time zone will be used). * @return a zone id accepted by {@link java.util.TimeZone#getTimeZone(String)}, - * or an empty String to indicate the server's default time zone + * or an empty String to indicate the scheduler's default time zone * @since 4.0 * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone) * @see java.util.TimeZone diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index 07075d9688a8..d4fd250bea29 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -430,14 +429,14 @@ protected void processScheduled(Scheduled scheduled, Method method, Object bean) Assert.isTrue(initialDelay.isNegative(), "'initialDelay' not supported for cron triggers"); processedSchedule = true; if (!Scheduled.CRON_DISABLED.equals(cron)) { - TimeZone timeZone; + CronTrigger trigger; if (StringUtils.hasText(zone)) { - timeZone = StringUtils.parseTimeZoneString(zone); + trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone)); } else { - timeZone = TimeZone.getDefault(); + trigger = new CronTrigger(cron); } - tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); + tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger))); } } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java index 0e6008de6253..5d58384a3943 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -168,7 +168,7 @@ private CronExpression(CronField seconds, CronField minutes, CronField hours, * the cron format */ public static CronExpression parse(String expression) { - Assert.hasLength(expression, "Expression string must not be empty"); + Assert.hasLength(expression, "Expression must not be empty"); expression = resolveMacros(expression); diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java index 7bcb8ed2fa74..88228a3c0835 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -39,6 +39,7 @@ public class CronTrigger implements Trigger { private final CronExpression expression; + @Nullable private final ZoneId zoneId; @@ -48,7 +49,8 @@ public class CronTrigger implements Trigger { * expression conventions */ public CronTrigger(String expression) { - this(expression, ZoneId.systemDefault()); + this.expression = CronExpression.parse(expression); + this.zoneId = null; } /** @@ -58,7 +60,9 @@ public CronTrigger(String expression) { * @param timeZone a time zone in which the trigger times will be generated */ public CronTrigger(String expression, TimeZone timeZone) { - this(expression, timeZone.toZoneId()); + this.expression = CronExpression.parse(expression); + Assert.notNull(timeZone, "TimeZone must not be null"); + this.zoneId = timeZone.toZoneId(); } /** @@ -70,10 +74,8 @@ public CronTrigger(String expression, TimeZone timeZone) { * @see CronExpression#parse(String) */ public CronTrigger(String expression, ZoneId zoneId) { - Assert.hasLength(expression, "Expression must not be empty"); - Assert.notNull(zoneId, "ZoneId must not be null"); - this.expression = CronExpression.parse(expression); + Assert.notNull(zoneId, "ZoneId must not be null"); this.zoneId = zoneId; } @@ -94,22 +96,23 @@ public String getExpression() { */ @Override public Instant nextExecution(TriggerContext triggerContext) { - Instant instant = triggerContext.lastCompletion(); - if (instant != null) { + Instant timestamp = triggerContext.lastCompletion(); + if (timestamp != null) { Instant scheduled = triggerContext.lastScheduledExecution(); - if (scheduled != null && instant.isBefore(scheduled)) { + if (scheduled != null && timestamp.isBefore(scheduled)) { // Previous task apparently executed too early... // Let's simply use the last calculated execution time then, // in order to prevent accidental re-fires in the same second. - instant = scheduled; + timestamp = scheduled; } } else { - instant = triggerContext.getClock().instant(); + timestamp = triggerContext.getClock().instant(); } - ZonedDateTime dateTime = ZonedDateTime.ofInstant(instant, this.zoneId); - ZonedDateTime next = this.expression.next(dateTime); - return (next != null ? next.toInstant() : null); + ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone()); + ZonedDateTime zonedTimestamp = ZonedDateTime.ofInstant(timestamp, zone); + ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp); + return (nextTimestamp != null ? nextTimestamp.toInstant() : null); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java index d1f58624c4c1..910ed03f5319 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronTriggerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -848,6 +848,7 @@ void daylightSavingMissingHour(Date localDateTime, TimeZone timeZone) { assertThat(nextExecutionTime).isEqualTo(this.calendar.getTime()); } + private static void roundup(Calendar calendar) { calendar.add(Calendar.SECOND, 1); calendar.set(Calendar.MILLISECOND, 0); @@ -861,9 +862,7 @@ private static void assertMatchesNextSecond(CronTrigger trigger, Calendar calend } private static TriggerContext getTriggerContext(Date lastCompletionTime) { - SimpleTriggerContext context = new SimpleTriggerContext(); - context.update(null, null, lastCompletionTime); - return context; + return new SimpleTriggerContext(null, null, lastCompletionTime); } From dd0d26b4ba395c964ef4ddc37906f41b5750bf95 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 6 Jan 2024 23:02:59 +0100 Subject: [PATCH 051/261] Refine exception handling for type not present versus access exception Includes TypeVariable bypass for reflection-free annotation retrieval. Includes info log message for annotation attribute retrieval failure. Closes gh-27182 (cherry picked from commit 70247c4a949b7e20cadd260b5fd46d880c183acb) --- .../core/SerializableTypeWrapper.java | 9 +- .../core/annotation/AnnotationUtils.java | 39 +- .../core/annotation/AnnotationsScanner.java | 4 +- .../core/annotation/AttributeMethods.java | 27 +- .../AnnotationIntrospectionFailureTests.java | 77 +- .../annotation/AttributeMethodsTests.java | 6 +- .../annotation/MergedAnnotationsTests.java | 727 ++++++++---------- 7 files changed, 417 insertions(+), 472 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java index 44f5719ad74b..344805fa1b95 100644 --- a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java +++ b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -214,7 +214,12 @@ else if (Type[].class == method.getReturnType() && ObjectUtils.isEmpty(args)) { return result; } - return ReflectionUtils.invokeMethod(method, this.provider.getType(), args); + Type type = this.provider.getType(); + if (type instanceof TypeVariable tv && method.getName().equals("getName")) { + // Avoid reflection for common comparison of type variables + return tv.getName(); + } + return ReflectionUtils.invokeMethod(method, type, args); } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 47baaae305ec..565e5055b70a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -757,7 +757,7 @@ public static boolean isInJavaLangAnnotationPackage(@Nullable String annotationT * Google App Engine's late arrival of {@code TypeNotPresentExceptionProxy} for * {@code Class} values (instead of early {@code Class.getAnnotations() failure}). *

This method not failing indicates that {@link #getAnnotationAttributes(Annotation)} - * won't failure either (when attempted later on). + * won't fail either (when attempted later on). * @param annotation the annotation to validate * @throws IllegalStateException if a declared {@code Class} attribute could not be read * @since 4.3.15 @@ -1056,8 +1056,7 @@ public static Object getValue(@Nullable Annotation annotation, @Nullable String return null; } catch (Throwable ex) { - rethrowAnnotationConfigurationException(ex); - handleIntrospectionFailure(annotation.getClass(), ex); + handleValueRetrievalFailure(annotation, ex); return null; } } @@ -1073,14 +1072,18 @@ public static Object getValue(@Nullable Annotation annotation, @Nullable String * @return the value returned from the method invocation * @since 5.3.24 */ - static Object invokeAnnotationMethod(Method method, Object annotation) { + @Nullable + static Object invokeAnnotationMethod(Method method, @Nullable Object annotation) { + if (annotation == null) { + return null; + } if (Proxy.isProxyClass(annotation.getClass())) { try { InvocationHandler handler = Proxy.getInvocationHandler(annotation); return handler.invoke(annotation, method, null); } catch (Throwable ex) { - // ignore and fall back to reflection below + // Ignore and fall back to reflection below } } return ReflectionUtils.invokeMethod(method, annotation); @@ -1114,20 +1117,32 @@ static void rethrowAnnotationConfigurationException(Throwable ex) { * @see #rethrowAnnotationConfigurationException * @see IntrospectionFailureLogger */ - static void handleIntrospectionFailure(@Nullable AnnotatedElement element, Throwable ex) { + static void handleIntrospectionFailure(AnnotatedElement element, Throwable ex) { rethrowAnnotationConfigurationException(ex); IntrospectionFailureLogger logger = IntrospectionFailureLogger.INFO; boolean meta = false; if (element instanceof Class clazz && Annotation.class.isAssignableFrom(clazz)) { - // Meta-annotation or (default) value lookup on an annotation type + // Meta-annotation introspection failure logger = IntrospectionFailureLogger.DEBUG; meta = true; } if (logger.isEnabled()) { - String message = meta ? - "Failed to meta-introspect annotation " : - "Failed to introspect annotations on "; - logger.log(message + element + ": " + ex); + logger.log("Failed to " + (meta ? "meta-introspect annotation " : "introspect annotations on ") + + element + ": " + ex); + } + } + + /** + * Handle the supplied value retrieval exception. + * @param annotation the annotation instance from which to retrieve the value + * @param ex the exception that we encountered + * @see #handleIntrospectionFailure + */ + private static void handleValueRetrievalFailure(Annotation annotation, Throwable ex) { + rethrowAnnotationConfigurationException(ex); + IntrospectionFailureLogger logger = IntrospectionFailureLogger.INFO; + if (logger.isEnabled()) { + logger.log("Failed to retrieve value from " + annotation + ": " + ex); } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index e6a8626e05ad..cb3ae366a901 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -453,7 +453,7 @@ static Annotation[] getDeclaredAnnotations(AnnotatedElement source, boolean defe for (int i = 0; i < annotations.length; i++) { Annotation annotation = annotations[i]; if (isIgnorable(annotation.annotationType()) || - !AttributeMethods.forAnnotationType(annotation.annotationType()).isValid(annotation)) { + !AttributeMethods.forAnnotationType(annotation.annotationType()).canLoad(annotation)) { annotations[i] = null; } else { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java index a828ebe44b5a..c56064b77a98 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -45,7 +45,7 @@ final class AttributeMethods { if (m1 != null && m2 != null) { return m1.getName().compareTo(m2.getName()); } - return m1 != null ? -1 : 1; + return (m1 != null ? -1 : 1); }; @@ -87,18 +87,26 @@ private AttributeMethods(@Nullable Class annotationType, M /** * Determine if values from the given annotation can be safely accessed without * causing any {@link TypeNotPresentException TypeNotPresentExceptions}. + *

This method is designed to cover Google App Engine's late arrival of such + * exceptions for {@code Class} values (instead of the more typical early + * {@code Class.getAnnotations() failure} on a regular JVM). * @param annotation the annotation to check * @return {@code true} if all values are present * @see #validate(Annotation) */ - boolean isValid(Annotation annotation) { + boolean canLoad(Annotation annotation) { assertAnnotation(annotation); for (int i = 0; i < size(); i++) { if (canThrowTypeNotPresentException(i)) { try { AnnotationUtils.invokeAnnotationMethod(get(i), annotation); } + catch (IllegalStateException ex) { + // Plain invocation failure to expose -> leave up to attribute retrieval + // (if any) where such invocation failure will be logged eventually. + } catch (Throwable ex) { + // TypeNotPresentException etc. -> annotation type not actually loadable. return false; } } @@ -108,13 +116,13 @@ boolean isValid(Annotation annotation) { /** * Check if values from the given annotation can be safely accessed without causing - * any {@link TypeNotPresentException TypeNotPresentExceptions}. In particular, - * this method is designed to cover Google App Engine's late arrival of such + * any {@link TypeNotPresentException TypeNotPresentExceptions}. + *

This method is designed to cover Google App Engine's late arrival of such * exceptions for {@code Class} values (instead of the more typical early - * {@code Class.getAnnotations() failure}). + * {@code Class.getAnnotations() failure} on a regular JVM). * @param annotation the annotation to validate * @throws IllegalStateException if a declared {@code Class} attribute could not be read - * @see #isValid(Annotation) + * @see #canLoad(Annotation) */ void validate(Annotation annotation) { assertAnnotation(annotation); @@ -123,6 +131,9 @@ void validate(Annotation annotation) { try { AnnotationUtils.invokeAnnotationMethod(get(i), annotation); } + catch (IllegalStateException ex) { + throw ex; + } catch (Throwable ex) { throw new IllegalStateException("Could not obtain annotation attribute value for " + get(i).getName() + " declared on " + annotation.annotationType(), ex); @@ -147,7 +158,7 @@ private void assertAnnotation(Annotation annotation) { @Nullable Method get(String name) { int index = indexOf(name); - return index != -1 ? this.attributeMethods[index] : null; + return (index != -1 ? this.attributeMethods[index] : null); } /** diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java index 0fa93c99e938..9750cd5ac2e8 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -43,55 +43,45 @@ class AnnotationIntrospectionFailureTests { @Test void filteredTypeThrowsTypeNotPresentException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleAnnotation = ClassUtils.forName( - WithExampleAnnotation.class.getName(), classLoader); - Annotation annotation = withExampleAnnotation.getAnnotations()[0]; + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleAnnotation.class.getName(), classLoader); + Annotation annotation = withAnnotation.getAnnotations()[0]; Method method = annotation.annotationType().getMethod("value"); method.setAccessible(true); - assertThatExceptionOfType(TypeNotPresentException.class).isThrownBy(() -> - ReflectionUtils.invokeMethod(method, annotation)) - .withCauseInstanceOf(ClassNotFoundException.class); + assertThatExceptionOfType(TypeNotPresentException.class) + .isThrownBy(() -> ReflectionUtils.invokeMethod(method, annotation)) + .withCauseInstanceOf(ClassNotFoundException.class); } @Test @SuppressWarnings("unchecked") void filteredTypeInMetaAnnotationWhenUsingAnnotatedElementUtilsHandlesException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleMetaAnnotation = ClassUtils.forName( - WithExampleMetaAnnotation.class.getName(), classLoader); - Class exampleAnnotationClass = (Class) ClassUtils.forName( - ExampleAnnotation.class.getName(), classLoader); - Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( - ExampleMetaAnnotation.class.getName(), classLoader); - assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes( - withExampleMetaAnnotation, exampleAnnotationClass)).isNull(); - assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes( - withExampleMetaAnnotation, exampleMetaAnnotationClass)).isNull(); - assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation, - exampleAnnotationClass)).isFalse(); - assertThat(AnnotatedElementUtils.hasAnnotation(withExampleMetaAnnotation, - exampleMetaAnnotationClass)).isFalse(); + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleMetaAnnotation.class.getName(), classLoader); + Class annotationClass = (Class) + ClassUtils.forName(ExampleAnnotation.class.getName(), classLoader); + Class metaAnnotationClass = (Class) + ClassUtils.forName(ExampleMetaAnnotation.class.getName(), classLoader); + assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes(withAnnotation, annotationClass)).isNull(); + assertThat(AnnotatedElementUtils.getMergedAnnotationAttributes(withAnnotation, metaAnnotationClass)).isNull(); + assertThat(AnnotatedElementUtils.hasAnnotation(withAnnotation, annotationClass)).isFalse(); + assertThat(AnnotatedElementUtils.hasAnnotation(withAnnotation, metaAnnotationClass)).isFalse(); } @Test @SuppressWarnings("unchecked") void filteredTypeInMetaAnnotationWhenUsingMergedAnnotationsHandlesException() throws Exception { - FilteringClassLoader classLoader = new FilteringClassLoader( - getClass().getClassLoader()); - Class withExampleMetaAnnotation = ClassUtils.forName( - WithExampleMetaAnnotation.class.getName(), classLoader); - Class exampleAnnotationClass = (Class) ClassUtils.forName( - ExampleAnnotation.class.getName(), classLoader); - Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( - ExampleMetaAnnotation.class.getName(), classLoader); - MergedAnnotations annotations = MergedAnnotations.from(withExampleMetaAnnotation); - assertThat(annotations.get(exampleAnnotationClass).isPresent()).isFalse(); - assertThat(annotations.get(exampleMetaAnnotationClass).isPresent()).isFalse(); - assertThat(annotations.isPresent(exampleMetaAnnotationClass)).isFalse(); - assertThat(annotations.isPresent(exampleAnnotationClass)).isFalse(); + FilteringClassLoader classLoader = new FilteringClassLoader(getClass().getClassLoader()); + Class withAnnotation = ClassUtils.forName(WithExampleMetaAnnotation.class.getName(), classLoader); + Class annotationClass = (Class) + ClassUtils.forName(ExampleAnnotation.class.getName(), classLoader); + Class metaAnnotationClass = (Class) + ClassUtils.forName(ExampleMetaAnnotation.class.getName(), classLoader); + MergedAnnotations annotations = MergedAnnotations.from(withAnnotation); + assertThat(annotations.get(annotationClass).isPresent()).isFalse(); + assertThat(annotations.get(metaAnnotationClass).isPresent()).isFalse(); + assertThat(annotations.isPresent(metaAnnotationClass)).isFalse(); + assertThat(annotations.isPresent(annotationClass)).isFalse(); } @@ -103,17 +93,16 @@ static class FilteringClassLoader extends OverridingClassLoader { @Override protected boolean isEligibleForOverriding(String className) { - return className.startsWith( - AnnotationIntrospectionFailureTests.class.getName()); + return className.startsWith(AnnotationIntrospectionFailureTests.class.getName()) || + className.startsWith("jdk.internal"); } @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.startsWith(AnnotationIntrospectionFailureTests.class.getName()) && - name.contains("Filtered")) { + protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + if (name.contains("Filtered") || name.startsWith("jdk.internal")) { throw new ClassNotFoundException(name); } - return super.loadClass(name, resolve); + return super.loadClassForOverriding(name); } } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java index 8a4acb7b7978..a2b885601f72 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,7 +112,7 @@ void isValidWhenHasTypeNotPresentExceptionReturnsFalse() { ClassValue annotation = mockAnnotation(ClassValue.class); given(annotation.value()).willThrow(TypeNotPresentException.class); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); - assertThat(attributes.isValid(annotation)).isFalse(); + assertThat(attributes.canLoad(annotation)).isFalse(); } @Test @@ -121,7 +121,7 @@ void isValidWhenDoesNotHaveTypeNotPresentExceptionReturnsTrue() { ClassValue annotation = mock(); given(annotation.value()).willReturn((Class) InputStream.class); AttributeMethods attributes = AttributeMethods.forAnnotationType(annotation.annotationType()); - assertThat(attributes.isValid(annotation)).isTrue(); + assertThat(attributes.canLoad(annotation)).isTrue(); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index f73654666c91..6378c6f3a312 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -88,47 +88,47 @@ class FluentSearchApiTests { @Test void preconditions() { assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.search(null)) - .withMessage("SearchStrategy must not be null"); + .isThrownBy(() -> MergedAnnotations.search(null)) + .withMessage("SearchStrategy must not be null"); Search search = MergedAnnotations.search(SearchStrategy.SUPERCLASS); assertThatIllegalArgumentException() - .isThrownBy(() -> search.withEnclosingClasses(null)) - .withMessage("Predicate must not be null"); + .isThrownBy(() -> search.withEnclosingClasses(null)) + .withMessage("Predicate must not be null"); assertThatIllegalStateException() - .isThrownBy(() -> search.withEnclosingClasses(Search.always)) - .withMessage("A custom 'searchEnclosingClass' predicate can only be combined with SearchStrategy.TYPE_HIERARCHY"); + .isThrownBy(() -> search.withEnclosingClasses(Search.always)) + .withMessage("A custom 'searchEnclosingClass' predicate can only be combined with SearchStrategy.TYPE_HIERARCHY"); assertThatIllegalArgumentException() - .isThrownBy(() -> search.withAnnotationFilter(null)) - .withMessage("AnnotationFilter must not be null"); + .isThrownBy(() -> search.withAnnotationFilter(null)) + .withMessage("AnnotationFilter must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> search.withRepeatableContainers(null)) - .withMessage("RepeatableContainers must not be null"); + .isThrownBy(() -> search.withRepeatableContainers(null)) + .withMessage("RepeatableContainers must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> search.from(null)) - .withMessage("AnnotatedElement must not be null"); + .isThrownBy(() -> search.from(null)) + .withMessage("AnnotatedElement must not be null"); } @Test void searchFromClassWithDefaultAnnotationFilterAndDefaultRepeatableContainers() { Stream> classes = MergedAnnotations.search(SearchStrategy.DIRECT) - .from(TransactionalComponent.class) - .stream() - .map(MergedAnnotation::getType); + .from(TransactionalComponent.class) + .stream() + .map(MergedAnnotation::getType); assertThat(classes).containsExactly(Transactional.class, Component.class, Indexed.class); } @Test void searchFromClassWithCustomAnnotationFilter() { Stream> classes = MergedAnnotations.search(SearchStrategy.DIRECT) - .withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed")) - .from(TransactionalComponent.class) - .stream() - .map(MergedAnnotation::getType); + .withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed")) + .from(TransactionalComponent.class) + .stream() + .map(MergedAnnotation::getType); assertThat(classes).containsExactly(Transactional.class, Component.class); } @@ -138,14 +138,14 @@ void searchFromClassWithCustomRepeatableContainers() { RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class); MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT) - .withRepeatableContainers(containers) - .from(HierarchyClass.class); + .withRepeatableContainers(containers) + .from(HierarchyClass.class); assertThat(annotations.stream(TestConfiguration.class)) - .map(annotation -> annotation.getString("location")) - .containsExactly("A", "B"); + .map(annotation -> annotation.getString("location")) + .containsExactly("A", "B"); assertThat(annotations.stream(TestConfiguration.class)) - .map(annotation -> annotation.getString("value")) - .containsExactly("A", "B"); + .map(annotation -> annotation.getString("value")) + .containsExactly("A", "B"); } /** @@ -205,9 +205,9 @@ void searchFromNonAnnotatedStaticNestedClassWithAnnotatedEnclosingClassWithEnclo .map(MergedAnnotation::getType); assertThat(classes).containsExactly(Component.class, Indexed.class); } - } + @Nested class ConventionBasedAnnotationAttributeOverrideTests { @@ -215,7 +215,7 @@ class ConventionBasedAnnotationAttributeOverrideTests { void getWithInheritedAnnotationsAttributesWithConventionBasedComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from(ConventionBasedComposedContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); @@ -227,7 +227,7 @@ void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnn // xmlConfigFiles can be used because it has an AliasFor annotation MergedAnnotation annotation = MergedAnnotations.from(HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); } @@ -238,7 +238,7 @@ void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnn // locations doesn't apply because it has no AliasFor annotation MergedAnnotation annotation = MergedAnnotations.from(HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass2.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty(); } @@ -259,7 +259,7 @@ void getWithTypeHierarchyWithSingleElementOverridingAnArrayViaConvention() { void getWithTypeHierarchyWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { MergedAnnotation annotation = MergedAnnotations.from(SpringApplicationConfigurationClass.class, SearchStrategy.TYPE_HIERARCHY) - .get(ContextConfiguration.class); + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty(); assertThat(annotation.getClassArray("classes")).containsExactly(Number.class); @@ -269,33 +269,32 @@ void getWithTypeHierarchyWithLocalAliasesThatConflictWithAttributesInMetaAnnotat void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaConvention() throws Exception { testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("postMappedWithPathAttribute")); } - } + @Test void fromPreconditions() { SearchStrategy strategy = SearchStrategy.DIRECT; RepeatableContainers containers = RepeatableContainers.standardRepeatables(); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, null, AnnotationFilter.PLAIN)) - .withMessage("RepeatableContainers must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, null, AnnotationFilter.PLAIN)) + .withMessage("RepeatableContainers must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, containers, null)) - .withMessage("AnnotationFilter must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), strategy, containers, null)) + .withMessage("AnnotationFilter must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], null, AnnotationFilter.PLAIN)) - .withMessage("RepeatableContainers must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], null, AnnotationFilter.PLAIN)) + .withMessage("RepeatableContainers must not be null"); assertThatIllegalArgumentException() - .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], containers, null)) - .withMessage("AnnotationFilter must not be null"); + .isThrownBy(() -> MergedAnnotations.from(getClass(), new Annotation[0], containers, null)) + .withMessage("AnnotationFilter must not be null"); } @Test void streamWhenFromNonAnnotatedClass() { - assertThat(MergedAnnotations.from(NonAnnotatedClass.class). - stream(TransactionalComponent.class)).isEmpty(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).stream(TransactionalComponent.class)).isEmpty(); } @Test @@ -315,14 +314,12 @@ void streamWhenFromClassWithMetaDepth2() { @Test void isPresentWhenFromNonAnnotatedClass() { - assertThat(MergedAnnotations.from(NonAnnotatedClass.class). - isPresent(Transactional.class)).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).isPresent(Transactional.class)).isFalse(); } @Test void isPresentWhenFromAnnotationClassWithMetaDepth0() { - assertThat(MergedAnnotations.from(TransactionalComponent.class). - isPresent(TransactionalComponent.class)).isFalse(); + assertThat(MergedAnnotations.from(TransactionalComponent.class).isPresent(TransactionalComponent.class)).isFalse(); } @Test @@ -334,8 +331,7 @@ void isPresentWhenFromAnnotationClassWithMetaDepth1() { @Test void isPresentWhenFromAnnotationClassWithMetaDepth2() { - MergedAnnotations annotations = MergedAnnotations.from( - ComposedTransactionalComponent.class); + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponent.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isFalse(); @@ -343,28 +339,24 @@ void isPresentWhenFromAnnotationClassWithMetaDepth2() { @Test void isPresentWhenFromClassWithMetaDepth0() { - assertThat(MergedAnnotations.from(TransactionalComponentClass.class).isPresent( - TransactionalComponent.class)).isTrue(); + assertThat(MergedAnnotations.from(TransactionalComponentClass.class).isPresent(TransactionalComponent.class)).isTrue(); } @Test void isPresentWhenFromSubclassWithMetaDepth0() { - assertThat(MergedAnnotations.from(SubTransactionalComponentClass.class).isPresent( - TransactionalComponent.class)).isFalse(); + assertThat(MergedAnnotations.from(SubTransactionalComponentClass.class).isPresent(TransactionalComponent.class)).isFalse(); } @Test void isPresentWhenFromClassWithMetaDepth1() { - MergedAnnotations annotations = MergedAnnotations.from( - TransactionalComponentClass.class); + MergedAnnotations annotations = MergedAnnotations.from(TransactionalComponentClass.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); } @Test void isPresentWhenFromClassWithMetaDepth2() { - MergedAnnotations annotations = MergedAnnotations.from( - ComposedTransactionalComponentClass.class); + MergedAnnotations annotations = MergedAnnotations.from(ComposedTransactionalComponentClass.class); assertThat(annotations.isPresent(Transactional.class)).isTrue(); assertThat(annotations.isPresent(Component.class)).isTrue(); assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isTrue(); @@ -395,35 +387,31 @@ void getRootWhenDirect() { @Test void getMetaTypes() { - MergedAnnotation annotation = MergedAnnotations.from( - ComposedTransactionalComponentClass.class).get( - TransactionalComponent.class); + MergedAnnotation annotation = MergedAnnotations.from(ComposedTransactionalComponentClass.class) + .get(TransactionalComponent.class); assertThat(annotation.getMetaTypes()).containsExactly( ComposedTransactionalComponent.class, TransactionalComponent.class); } @Test void collectMultiValueMapFromNonAnnotatedClass() { - MultiValueMap map = MergedAnnotations.from( - NonAnnotatedClass.class).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + MultiValueMap map = MergedAnnotations.from(NonAnnotatedClass.class) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).isEmpty(); } @Test void collectMultiValueMapFromClassWithLocalAnnotation() { - MultiValueMap map = MergedAnnotations.from(TxConfig.class).stream( - Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains(entry("value", Arrays.asList("TxConfig"))); + MultiValueMap map = MergedAnnotations.from(TxConfig.class) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", List.of("TxConfig"))); } @Test void collectMultiValueMapFromClassWithLocalComposedAnnotationAndInheritedAnnotation() { MultiValueMap map = MergedAnnotations.from( - SubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + SubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).contains( entry("qualifier", Arrays.asList("composed2", "transactionManager"))); } @@ -431,19 +419,17 @@ void collectMultiValueMapFromClassWithLocalComposedAnnotationAndInheritedAnnotat @Test void collectMultiValueMapFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - SubSubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains(entry("qualifier", Arrays.asList("transactionManager"))); + SubSubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", List.of("transactionManager"))); } @Test void collectMultiValueMapFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - SubSubClassWithInheritedComposedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains(entry("qualifier", Arrays.asList("composed1"))); + SubSubClassWithInheritedComposedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", List.of("composed1"))); } /** @@ -455,10 +441,10 @@ void collectMultiValueMapFavorsInheritedComposedAnnotationsOverMoreLocallyDeclar */ @Test void collectMultiValueMapFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { - MultiValueMap map = MergedAnnotations.from(DerivedTxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains(entry("value", Arrays.asList("DerivedTxConfig"))); + MultiValueMap map = MergedAnnotations.from( + DerivedTxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", List.of("DerivedTxConfig"))); } /** @@ -468,24 +454,23 @@ void collectMultiValueMapFromClassWithLocalAnnotationThatShadowsAnnotationFromSu @Test void collectMultiValueMapFromClassWithMultipleComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( - TxFromMultipleComposedAnnotations.class, - SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( - MergedAnnotationCollectors.toMultiValueMap()); + TxFromMultipleComposedAnnotations.class, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); assertThat(map).contains( entry("value", Arrays.asList("TxInheritedComposed", "TxComposed"))); } @Test void getWithInheritedAnnotationsFromClassWithLocalAnnotation() { - MergedAnnotation annotation = MergedAnnotations.from(TxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from( + TxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getString("value")).isEqualTo("TxConfig"); } @Test void getWithInheritedAnnotationsFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { - MergedAnnotation annotation = MergedAnnotations.from(DerivedTxConfig.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from( + DerivedTxConfig.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getString("value")).isEqualTo("DerivedTxConfig"); } @@ -499,53 +484,46 @@ void getWithInheritedAnnotationsFromMetaCycleAnnotatedClassWithMissingTargetMeta @Test void getWithInheritedAnnotationsFavorsLocalComposedAnnotationOverInheritedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - SubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isTrue(); } @Test void getWithInheritedAnnotationsFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubSubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isFalse(); } @Test void getWithInheritedAnnotationsFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubClassWithInheritedComposedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + SubSubClassWithInheritedComposedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.getBoolean("readOnly")).isFalse(); } @Test void getWithInheritedAnnotationsFromInterfaceImplementedBySuperclass() { MergedAnnotation annotation = MergedAnnotations.from( - ConcreteClassWithInheritedAnnotation.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + ConcreteClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.isPresent()).isFalse(); } @Test void getWithInheritedAnnotationsFromInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - InheritedAnnotationInterface.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + InheritedAnnotationInterface.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); } @Test void getWithInheritedAnnotationsFromNonInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - NonInheritedAnnotationInterface.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(Order.class); + NonInheritedAnnotationInterface.class, SearchStrategy.INHERITED_ANNOTATIONS).get(Order.class); assertThat(annotation.isPresent()).isTrue(); } - @Test void withInheritedAnnotationsFromAliasedComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( @@ -567,15 +545,11 @@ void withInheritedAnnotationsFromAliasedValueComposedAnnotation() { @Test void getWithInheritedAnnotationsFromImplicitAliasesInMetaAnnotationOnComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - ComposedImplicitAliasesContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get( - ImplicitAliasesContextConfiguration.class); - assertThat(annotation.getStringArray("groovyScripts")).containsExactly("A.xml", - "B.xml"); - assertThat(annotation.getStringArray("xmlFiles")).containsExactly("A.xml", - "B.xml"); - assertThat(annotation.getStringArray("locations")).containsExactly("A.xml", - "B.xml"); + ComposedImplicitAliasesContextConfigurationClass.class, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ImplicitAliasesContextConfiguration.class); + assertThat(annotation.getStringArray("groovyScripts")).containsExactly("A.xml", "B.xml"); + assertThat(annotation.getStringArray("xmlFiles")).containsExactly("A.xml", "B.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("A.xml", "B.xml"); assertThat(annotation.getStringArray("value")).containsExactly("A.xml", "B.xml"); } @@ -615,8 +589,8 @@ void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSkippedLevelWit } private void testGetWithInherited(Class element, String... expected) { - MergedAnnotation annotation = MergedAnnotations.from(element, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + MergedAnnotation annotation = MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEqualTo(expected); assertThat(annotation.getStringArray("value")).isEqualTo(expected); assertThat(annotation.getClassArray("classes")).isEmpty(); @@ -625,8 +599,8 @@ private void testGetWithInherited(Class element, String... expected) { @Test void getWithInheritedAnnotationsFromShadowedAliasComposedAnnotation() { MergedAnnotation annotation = MergedAnnotations.from( - ShadowedAliasComposedContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + ShadowedAliasComposedContextConfigurationClass.class, SearchStrategy.INHERITED_ANNOTATIONS) + .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); } @@ -674,8 +648,7 @@ void getWithTypeHierarchyFromSubNonInheritedAnnotationInterface() { @Test void getWithTypeHierarchyFromSubSubNonInheritedAnnotationInterface() { MergedAnnotation annotation = MergedAnnotations.from( - SubSubNonInheritedAnnotationInterface.class, - SearchStrategy.TYPE_HIERARCHY).get(Order.class); + SubSubNonInheritedAnnotationInterface.class, SearchStrategy.TYPE_HIERARCHY).get(Order.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(2); } @@ -692,27 +665,22 @@ void getWithTypeHierarchyInheritedFromInterfaceMethod() throws Exception { void streamWithTypeHierarchyInheritedFromSuperInterfaceMethod() throws Exception { Method method = Hello2Impl.class.getMethod("method"); long count = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY) - .from(method) - .stream(TestAnnotation1.class) - .count(); + .from(method).stream(TestAnnotation1.class).count(); assertThat(count).isEqualTo(1); } @Test void getWithTypeHierarchyInheritedFromAbstractMethod() throws NoSuchMethodException { Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handle"); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } @Test void getWithTypeHierarchyInheritedFromBridgedMethod() throws NoSuchMethodException { - Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( - "handleParameterized", String.class); - MergedAnnotation annotation = MergedAnnotations.from(method, - SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleParameterized", String.class); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } @@ -740,16 +708,14 @@ void getWithTypeHierarchyFromBridgeMethod() { @Test void getWithTypeHierarchyFromClassWithMetaAndLocalTxConfig() { MergedAnnotation annotation = MergedAnnotations.from( - MetaAndLocalTxConfigClass.class, SearchStrategy.TYPE_HIERARCHY).get( - Transactional.class); + MetaAndLocalTxConfigClass.class, SearchStrategy.TYPE_HIERARCHY).get(Transactional.class); assertThat(annotation.getString("qualifier")).isEqualTo("localTxMgr"); } @Test void getWithTypeHierarchyFromClassWithAttributeAliasesInTargetAnnotation() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - AliasedTransactionalComponentClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + AliasedTransactionalComponentClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); AliasedTransactional synthesizedAnnotation = mergedAnnotation.synthesize(); String qualifier = "aliasForQualifier"; assertThat(mergedAnnotation.getString("value")).isEqualTo(qualifier); @@ -761,8 +727,7 @@ void getWithTypeHierarchyFromClassWithAttributeAliasesInTargetAnnotation() { @Test // gh-23767 void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - ComposedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + ComposedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); assertThat(mergedAnnotation.getString("value")).isEqualTo("anotherTransactionManager"); assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("anotherTransactionManager"); } @@ -770,8 +735,7 @@ void getWithTypeHierarchyFromClassWithComposedMetaTransactionalAnnotation() { @Test // gh-23767 void getWithTypeHierarchyFromClassWithMetaMetaAliasedTransactional() { MergedAnnotation mergedAnnotation = MergedAnnotations.from( - MetaMetaAliasedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get( - AliasedTransactional.class); + MetaMetaAliasedTransactionalClass.class, SearchStrategy.TYPE_HIERARCHY).get(AliasedTransactional.class); assertThat(mergedAnnotation.getString("value")).isEqualTo("meta"); assertThat(mergedAnnotation.getString("qualifier")).isEqualTo("meta"); } @@ -807,51 +771,46 @@ private MergedAnnotation testGetWithTypeHierarchy(Class element, String... @Test void getWithTypeHierarchyWhenMultipleMetaAnnotationsHaveClashingAttributeNames() { MergedAnnotations annotations = MergedAnnotations.from( - AliasedComposedContextConfigurationAndTestPropertySourceClass.class, - SearchStrategy.TYPE_HIERARCHY); + AliasedComposedContextConfigurationAndTestPropertySourceClass.class, SearchStrategy.TYPE_HIERARCHY); MergedAnnotation contextConfig = annotations.get(ContextConfiguration.class); assertThat(contextConfig.getStringArray("locations")).containsExactly("test.xml"); assertThat(contextConfig.getStringArray("value")).containsExactly("test.xml"); MergedAnnotation testPropSource = annotations.get(TestPropertySource.class); - assertThat(testPropSource.getStringArray("locations")).containsExactly( - "test.properties"); - assertThat(testPropSource.getStringArray("value")).containsExactly( - "test.properties"); + assertThat(testPropSource.getStringArray("locations")).containsExactly("test.properties"); + assertThat(testPropSource.getStringArray("value")).containsExactly("test.properties"); } @Test void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { - testGetWithTypeHierarchyWebMapping( - WebController.class.getMethod("getMappedWithValueAttribute")); - testGetWithTypeHierarchyWebMapping( - WebController.class.getMethod("getMappedWithPathAttribute")); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); } private void testGetWithTypeHierarchyWebMapping(AnnotatedElement element) { - MergedAnnotation annotation = MergedAnnotations.from(element, - SearchStrategy.TYPE_HIERARCHY).get(RequestMapping.class); + MergedAnnotation annotation = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY) + .get(RequestMapping.class); assertThat(annotation.getStringArray("value")).containsExactly("/test"); assertThat(annotation.getStringArray("path")).containsExactly("/test"); } @Test - void getDirectWithJavaxAnnotationType() throws Exception { - assertThat(MergedAnnotations.from(ResourceHolder.class).get( - Resource.class).getString("name")).isEqualTo("x"); + void getDirectWithJavaxAnnotationType() { + assertThat(MergedAnnotations.from(ResourceHolder.class).get(Resource.class) + .getString("name")).isEqualTo("x"); } @Test void streamInheritedFromClassWithInterface() throws Exception { Method method = TransactionalServiceImpl.class.getMethod("doIt"); - assertThat(MergedAnnotations.from(method, SearchStrategy.INHERITED_ANNOTATIONS).stream( - Transactional.class)).isEmpty(); + assertThat(MergedAnnotations.from(method, SearchStrategy.INHERITED_ANNOTATIONS) + .stream(Transactional.class)).isEmpty(); } @Test void streamTypeHierarchyFromClassWithInterface() throws Exception { Method method = TransactionalServiceImpl.class.getMethod("doIt"); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).stream( - Transactional.class)).hasSize(1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) + .stream(Transactional.class)).hasSize(1); } @Test @@ -860,8 +819,8 @@ void getFromMethodWithMethodAnnotationOnLeaf() throws Exception { Method method = Leaf.class.getMethod("annotatedOnLeaf"); assertThat(method.getAnnotation(Order.class)).isNotNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(0); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(0); } @Test @@ -869,8 +828,8 @@ void getFromMethodWithAnnotationOnMethodInInterface() throws Exception { Method method = Leaf.class.getMethod("fromInterfaceImplementedByRoot"); assertThat(method.getAnnotation(Order.class)).isNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(0); } @Test @@ -878,8 +837,8 @@ void getFromMethodWithMetaAnnotationOnLeaf() throws Exception { Method method = Leaf.class.getMethod("metaAnnotatedOnLeaf"); assertThat(method.getAnnotation(Order.class)).isNull(); assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(1); - assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get( - Order.class).getDistance()).isEqualTo(1); + assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class) + .getDistance()).isEqualTo(1); } @Test @@ -1214,7 +1173,7 @@ private Object getSuperClassSourceWithTypeIn(Class clazz, List ele } @Test - void synthesizeWithoutAttributeAliases() throws Exception { + void synthesizeWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Component synthesizedComponent = MergedAnnotation.from(component).synthesize(); @@ -1631,114 +1590,123 @@ void synthesizeShouldNotResynthesizeAlreadySynthesizedAnnotations() throws Excep } @Test - void synthesizeWhenAliasForIsMissingAttributeDeclaration() throws Exception { + void synthesizeWhenAliasForIsMissingAttributeDeclaration() { AliasForWithMissingAttributeDeclaration annotation = AliasForWithMissingAttributeDeclarationClass.class.getAnnotation( AliasForWithMissingAttributeDeclaration.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") - .withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName()) - .withMessageContaining("points to itself"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") + .withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName()) + .withMessageContaining("points to itself"); } @Test - void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() throws Exception { - AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( - AliasForWithDuplicateAttributeDeclaration.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation") - .withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName()) - .withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); + void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() { + AliasForWithDuplicateAttributeDeclaration annotation = + AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( + AliasForWithDuplicateAttributeDeclaration.class); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation") + .withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName()) + .withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); } @Test - void synthesizeWhenAttributeAliasForNonexistentAttribute() throws Exception { + void synthesizeWhenAttributeAliasForNonexistentAttribute() { AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation( AliasForNonexistentAttribute.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") - .withMessageContaining(AliasForNonexistentAttribute.class.getName()) - .withMessageContaining("declares an alias for 'bar' which is not present"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") + .withMessageContaining(AliasForNonexistentAttribute.class.getName()) + .withMessageContaining("declares an alias for 'bar' which is not present"); } @Test - void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() throws Exception { + void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() { AliasForWithMirroredAliasForWrongAttribute annotation = AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation( AliasForWithMirroredAliasForWrongAttribute.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessage("@AliasFor declaration on attribute 'bar' in annotation [" - + AliasForWithMirroredAliasForWrongAttribute.class.getName() - + "] declares an alias for 'quux' which is not present."); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessage("@AliasFor declaration on attribute 'bar' in annotation [" + + AliasForWithMirroredAliasForWrongAttribute.class.getName() + + "] declares an alias for 'quux' which is not present."); } @Test - void synthesizeWhenAttributeAliasForAttributeOfDifferentType() throws Exception { + void synthesizeWhenAttributeAliasForAttributeOfDifferentType() { AliasForAttributeOfDifferentType annotation = AliasForAttributeOfDifferentTypeClass.class.getAnnotation( AliasForAttributeOfDifferentType.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForAttributeOfDifferentType.class.getName()) - .withMessageContaining("attribute 'foo'") - .withMessageContaining("attribute 'bar'") - .withMessageContaining("same return type"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForAttributeOfDifferentType.class.getName()) + .withMessageContaining("attribute 'foo'") + .withMessageContaining("attribute 'bar'") + .withMessageContaining("same return type"); } @Test - void synthesizeWhenAttributeAliasForWithMissingDefaultValues() throws Exception { + void synthesizeWhenAttributeAliasForWithMissingDefaultValues() { AliasForWithMissingDefaultValues annotation = AliasForWithMissingDefaultValuesClass.class.getAnnotation( AliasForWithMissingDefaultValues.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForWithMissingDefaultValues.class.getName()) - .withMessageContaining("attribute 'foo' in annotation") - .withMessageContaining("attribute 'bar' in annotation") - .withMessageContaining("default values"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForWithMissingDefaultValues.class.getName()) + .withMessageContaining("attribute 'foo' in annotation") + .withMessageContaining("attribute 'bar' in annotation") + .withMessageContaining("default values"); } @Test - void synthesizeWhenAttributeAliasForAttributeWithDifferentDefaultValue() throws Exception { + void synthesizeWhenAttributeAliasForAttributeWithDifferentDefaultValue() { AliasForAttributeWithDifferentDefaultValue annotation = AliasForAttributeWithDifferentDefaultValueClass.class.getAnnotation( AliasForAttributeWithDifferentDefaultValue.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("Misconfigured aliases") - .withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getName()) - .withMessageContaining("attribute 'foo' in annotation") - .withMessageContaining("attribute 'bar' in annotation") - .withMessageContaining("same default value"); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("Misconfigured aliases") + .withMessageContaining(AliasForAttributeWithDifferentDefaultValue.class.getName()) + .withMessageContaining("attribute 'foo' in annotation") + .withMessageContaining("attribute 'bar' in annotation") + .withMessageContaining("same default value"); } @Test - void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() throws Exception { + void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() { AliasedComposedTestConfigurationNotMetaPresent annotation = AliasedComposedTestConfigurationNotMetaPresentClass.class.getAnnotation( AliasedComposedTestConfigurationNotMetaPresent.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(annotation)) - .withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation") - .withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName()) - .withMessageContaining("declares an alias for attribute 'location' in annotation") - .withMessageContaining(TestConfiguration.class.getName()) - .withMessageContaining("not meta-present"); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)) + .withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation") + .withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName()) + .withMessageContaining("declares an alias for attribute 'location' in annotation") + .withMessageContaining(TestConfiguration.class.getName()) + .withMessageContaining("not meta-present"); } @Test - void synthesizeWithImplicitAliases() throws Exception { + void synthesizeWithImplicitAliases() { testSynthesisWithImplicitAliases(ValueImplicitAliasesTestConfigurationClass.class, "value"); testSynthesisWithImplicitAliases(Location1ImplicitAliasesTestConfigurationClass.class, "location1"); testSynthesisWithImplicitAliases(XmlImplicitAliasesTestConfigurationClass.class, "xmlFile"); testSynthesisWithImplicitAliases(GroovyImplicitAliasesSimpleTestConfigurationClass.class, "groovyScript"); } - private void testSynthesisWithImplicitAliases(Class clazz, String expected) throws Exception { + private void testSynthesisWithImplicitAliases(Class clazz, String expected) { ImplicitAliasesTestConfiguration config = clazz.getAnnotation(ImplicitAliasesTestConfiguration.class); assertThat(config).isNotNull(); ImplicitAliasesTestConfiguration synthesized = MergedAnnotation.from(config).synthesize(); @@ -1750,8 +1718,7 @@ private void testSynthesisWithImplicitAliases(Class clazz, String expected) t } @Test - void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() - throws Exception { + void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() { testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( ValueImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, "value"); @@ -1763,8 +1730,7 @@ void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() "xmlFile"); } - private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( - Class clazz, String expected) { + private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted(Class clazz, String expected) { ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration config = clazz.getAnnotation( ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class); assertThat(config).isNotNull(); @@ -1777,7 +1743,7 @@ private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( } @Test - void synthesizeWithImplicitAliasesForAliasPair() throws Exception { + void synthesizeWithImplicitAliasesForAliasPair() { ImplicitAliasesForAliasPairTestConfiguration config = ImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( ImplicitAliasesForAliasPairTestConfiguration.class); @@ -1788,7 +1754,7 @@ void synthesizeWithImplicitAliasesForAliasPair() throws Exception { } @Test - void synthesizeWithTransitiveImplicitAliases() throws Exception { + void synthesizeWithTransitiveImplicitAliases() { TransitiveImplicitAliasesTestConfiguration config = TransitiveImplicitAliasesTestConfigurationClass.class.getAnnotation( TransitiveImplicitAliasesTestConfiguration.class); @@ -1799,70 +1765,69 @@ void synthesizeWithTransitiveImplicitAliases() throws Exception { } @Test - void synthesizeWithTransitiveImplicitAliasesForAliasPair() throws Exception { + void synthesizeWithTransitiveImplicitAliasesForAliasPair() { TransitiveImplicitAliasesForAliasPairTestConfiguration config = TransitiveImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( TransitiveImplicitAliasesForAliasPairTestConfiguration.class); - TransitiveImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from( - config).synthesize(); + TransitiveImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from(config).synthesize(); assertSynthesized(synthesized); assertThat(synthesized.xml()).isEqualTo("test.xml"); assertThat(synthesized.groovy()).isEqualTo("test.xml"); } @Test - void synthesizeWithImplicitAliasesWithMissingDefaultValues() throws Exception { + void synthesizeWithImplicitAliasesWithMissingDefaultValues() { Class clazz = ImplicitAliasesWithMissingDefaultValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithMissingDefaultValuesTestConfiguration.class; - ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Misconfigured aliases:") - .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("default values"); + ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("default values"); } @Test - void synthesizeWithImplicitAliasesWithDifferentDefaultValues() - throws Exception { + void synthesizeWithImplicitAliasesWithDifferentDefaultValues() { Class clazz = ImplicitAliasesWithDifferentDefaultValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithDifferentDefaultValuesTestConfiguration.class; - ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Misconfigured aliases:") - .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") - .withMessageContaining("same default value"); + ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Misconfigured aliases:") + .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") + .withMessageContaining("same default value"); } @Test - void synthesizeWithImplicitAliasesWithDuplicateValues() throws Exception { + void synthesizeWithImplicitAliasesWithDuplicateValues() { Class clazz = ImplicitAliasesWithDuplicateValuesTestConfigurationClass.class; Class annotationType = ImplicitAliasesWithDuplicateValuesTestConfiguration.class; - ImplicitAliasesWithDuplicateValuesTestConfiguration config = clazz.getAnnotation( - annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(clazz, config)) - .withMessageStartingWith("Different @AliasFor mirror values for annotation") - .withMessageContaining(annotationType.getName()) - .withMessageContaining("declared on class") - .withMessageContaining(clazz.getName()) - .withMessageContaining("are declared with values of"); + ImplicitAliasesWithDuplicateValuesTestConfiguration config = clazz.getAnnotation(annotationType); + + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(clazz, config)) + .withMessageStartingWith("Different @AliasFor mirror values for annotation") + .withMessageContaining(annotationType.getName()) + .withMessageContaining("declared on class") + .withMessageContaining(clazz.getName()) + .withMessageContaining("are declared with values of"); } @Test - void synthesizeFromMapWithoutAttributeAliases() throws Exception { + void synthesizeFromMapWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Map map = Collections.singletonMap("value", "webController"); MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); + Component synthesizedComponent = annotation.synthesize(); assertSynthesized(synthesizedComponent); assertThat(synthesizedComponent.value()).isEqualTo("webController"); @@ -1870,14 +1835,13 @@ void synthesizeFromMapWithoutAttributeAliases() throws Exception { @Test @SuppressWarnings("unchecked") - void synthesizeFromMapWithNestedMap() throws Exception { + void synthesizeFromMapWithNestedMap() { ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation( ComponentScanSingleFilter.class); assertThat(componentScan).isNotNull(); assertThat(componentScan.value().pattern()).isEqualTo("*Foo"); Map map = MergedAnnotation.from(componentScan).asMap( - annotation -> new LinkedHashMap<>(), - Adapt.ANNOTATION_TO_MAP); + annotation -> new LinkedHashMap<>(), Adapt.ANNOTATION_TO_MAP); Map filterMap = (Map) map.get("value"); assertThat(filterMap.get("pattern")).isEqualTo("*Foo"); filterMap.put("pattern", "newFoo"); @@ -1891,13 +1855,11 @@ void synthesizeFromMapWithNestedMap() throws Exception { @Test @SuppressWarnings("unchecked") - void synthesizeFromMapWithNestedArrayOfMaps() throws Exception { - ComponentScan componentScan = ComponentScanClass.class.getAnnotation( - ComponentScan.class); + void synthesizeFromMapWithNestedArrayOfMaps() { + ComponentScan componentScan = ComponentScanClass.class.getAnnotation(ComponentScan.class); assertThat(componentScan).isNotNull(); Map map = MergedAnnotation.from(componentScan).asMap( - annotation -> new LinkedHashMap<>(), - Adapt.ANNOTATION_TO_MAP); + annotation -> new LinkedHashMap<>(), Adapt.ANNOTATION_TO_MAP); Map[] filters = (Map[]) map.get("excludeFilters"); List patterns = Arrays.stream(filters).map( m -> (String) m.get("pattern")).toList(); @@ -1906,18 +1868,16 @@ void synthesizeFromMapWithNestedArrayOfMaps() throws Exception { filters[0].put("enigma", 42); filters[1].put("pattern", "newBar"); filters[1].put("enigma", 42); - MergedAnnotation annotation = MergedAnnotation.of( - ComponentScan.class, map); + MergedAnnotation annotation = MergedAnnotation.of(ComponentScan.class, map); ComponentScan synthesizedComponentScan = annotation.synthesize(); assertSynthesized(synthesizedComponentScan); - assertThat(Arrays.stream(synthesizedComponentScan.excludeFilters()).map( - Filter::pattern)).containsExactly("newFoo", "newBar"); + assertThat(Arrays.stream(synthesizedComponentScan.excludeFilters()).map(Filter::pattern)) + .containsExactly("newFoo", "newBar"); } @Test - void synthesizeFromDefaultsWithoutAttributeAliases() throws Exception { - MergedAnnotation annotation = MergedAnnotation.of( - AnnotationWithDefaults.class); + void synthesizeFromDefaultsWithoutAttributeAliases() { + MergedAnnotation annotation = MergedAnnotation.of(AnnotationWithDefaults.class); AnnotationWithDefaults synthesized = annotation.synthesize(); assertThat(synthesized.text()).isEqualTo("enigma"); assertThat(synthesized.predicate()).isTrue(); @@ -1925,51 +1885,45 @@ void synthesizeFromDefaultsWithoutAttributeAliases() throws Exception { } @Test - void synthesizeFromDefaultsWithAttributeAliases() throws Exception { - MergedAnnotation annotation = MergedAnnotation.of( - TestConfiguration.class); + void synthesizeFromDefaultsWithAttributeAliases() { + MergedAnnotation annotation = MergedAnnotation.of(TestConfiguration.class); TestConfiguration synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEmpty(); assertThat(synthesized.location()).isEmpty(); } @Test - void synthesizeWhenAttributeAliasesWithDifferentValues() throws Exception { + void synthesizeWhenAttributeAliasesWithDifferentValues() { assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> MergedAnnotation.from(TestConfigurationMismatch.class.getAnnotation(TestConfiguration.class)).synthesize()); } @Test - void synthesizeFromMapWithMinimalAttributesWithAttributeAliases() - throws Exception { + void synthesizeFromMapWithMinimalAttributesWithAttributeAliases() { Map map = Collections.singletonMap("location", "test.xml"); - MergedAnnotation annotation = MergedAnnotation.of( - TestConfiguration.class, map); + MergedAnnotation annotation = MergedAnnotation.of(TestConfiguration.class, map); TestConfiguration synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEqualTo("test.xml"); assertThat(synthesized.location()).isEqualTo("test.xml"); } @Test - void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() - throws Exception { + void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() { synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( Collections.singletonMap("value", "/foo")); synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( Collections.singletonMap("path", "/foo")); } - private void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( - Map map) { - MergedAnnotation annotation = MergedAnnotation.of(GetMapping.class, - map); + private void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements(Map map) { + MergedAnnotation annotation = MergedAnnotation.of(GetMapping.class, map); GetMapping synthesized = annotation.synthesize(); assertThat(synthesized.value()).isEqualTo("/foo"); assertThat(synthesized.path()).isEqualTo("/foo"); } @Test - void synthesizeFromMapWithImplicitAttributeAliases() throws Exception { + void synthesizeFromMapWithImplicitAttributeAliases() { testSynthesisFromMapWithImplicitAliases("value"); testSynthesisFromMapWithImplicitAliases("location1"); testSynthesisFromMapWithImplicitAliases("location2"); @@ -1978,13 +1932,12 @@ void synthesizeFromMapWithImplicitAttributeAliases() throws Exception { testSynthesisFromMapWithImplicitAliases("groovyScript"); } - private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) - throws Exception { - Map map = Collections.singletonMap(attributeNameAndValue, - attributeNameAndValue); + private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) { + Map map = Collections.singletonMap(attributeNameAndValue, attributeNameAndValue); MergedAnnotation annotation = MergedAnnotation.of( ImplicitAliasesTestConfiguration.class, map); ImplicitAliasesTestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo(attributeNameAndValue); assertThat(synthesized.location1()).isEqualTo(attributeNameAndValue); assertThat(synthesized.location2()).isEqualTo(attributeNameAndValue); @@ -1994,12 +1947,12 @@ private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValu } @Test - void synthesizeFromMapWithMissingAttributeValue() throws Exception { + void synthesizeFromMapWithMissingAttributeValue() { testMissingTextAttribute(Collections.emptyMap()); } @Test - void synthesizeFromMapWithNullAttributeValue() throws Exception { + void synthesizeFromMapWithNullAttributeValue() { Map map = Collections.singletonMap("text", null); assertThat(map).containsKey("text"); testMissingTextAttribute(map); @@ -2008,12 +1961,12 @@ void synthesizeFromMapWithNullAttributeValue() throws Exception { private void testMissingTextAttribute(Map attributes) { assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> MergedAnnotation.of(AnnotationWithoutDefaults.class, attributes).synthesize().text()) - .withMessage("No value found for attribute named 'text' in merged annotation " + - AnnotationWithoutDefaults.class.getName()); + .withMessage("No value found for attribute named 'text' in merged annotation " + + AnnotationWithoutDefaults.class.getName()); } @Test - void synthesizeFromMapWithAttributeOfIncorrectType() throws Exception { + void synthesizeFromMapWithAttributeOfIncorrectType() { Map map = Collections.singletonMap("value", 42L); MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); assertThatIllegalStateException().isThrownBy(() -> annotation.synthesize().value()) @@ -2023,10 +1976,11 @@ void synthesizeFromMapWithAttributeOfIncorrectType() throws Exception { } @Test - void synthesizeFromAnnotationAttributesWithoutAttributeAliases() throws Exception { + void synthesizeFromAnnotationAttributesWithoutAttributeAliases() { Component component = WebController.class.getAnnotation(Component.class); assertThat(component).isNotNull(); Map attributes = MergedAnnotation.from(component).asMap(); + Component synthesized = MergedAnnotation.of(Component.class, attributes).synthesize(); assertSynthesized(synthesized); assertThat(synthesized).isEqualTo(component); @@ -2060,47 +2014,41 @@ void toStringForSynthesizedAnnotations() throws Exception { private void assertToStringForWebMappingWithPathAndValue(RequestMapping webMapping) { assertThat(webMapping.toString()) - .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(") - .contains( - // Strings - "value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", - // Characters - "ch='X'", "chars={'X'}", - // Enums - "method={GET, POST}", - // Classes - "clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class", - "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}", - // Bytes - "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}", - // Shorts - "shortValue=9876", "shorts={9876}", - // Longs - "longValue=42L", "longs={42L}", - // Floats - "floatValue=3.14f", "floats={3.14f}", - // Doubles - "doubleValue=99.999d", "doubles={99.999d}" - ) - .endsWith(")"); + .startsWith("@org.springframework.core.annotation.MergedAnnotationsTests.RequestMapping(") + .contains( + // Strings + "value={\"/test\"}", "path={\"/test\"}", "name=\"bar\"", + // Characters + "ch='X'", "chars={'X'}", + // Enums + "method={GET, POST}", + // Classes + "clazz=org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod.class", + "classes={int[][].class, org.springframework.core.annotation.MergedAnnotationsTests.RequestMethod[].class}", + // Bytes + "byteValue=(byte) 0xFF", "bytes={(byte) 0xFF}", + // Shorts + "shortValue=9876", "shorts={9876}", + // Longs + "longValue=42L", "longs={42L}", + // Floats + "floatValue=3.14f", "floats={3.14f}", + // Doubles + "doubleValue=99.999d", "doubles={99.999d}" + ) + .endsWith(")"); } @Test void equalsForSynthesizedAnnotations() throws Exception { - Method methodWithPath = WebController.class.getMethod( - "handleMappedWithPathAttribute"); - RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( - RequestMapping.class); + Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation(RequestMapping.class); assertThat(webMappingWithAliases).isNotNull(); - Method methodWithPathAndValue = WebController.class.getMethod( - "handleMappedWithSamePathAndValueAttributes"); - RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( - RequestMapping.class); + Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(RequestMapping.class); assertThat(webMappingWithPathAndValue).isNotNull(); - RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( - webMappingWithAliases).synthesize(); - RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( - webMappingWithPathAndValue).synthesize(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from(webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMappingWithPathAndValue).synthesize(); // Equality amongst standard annotations assertThat(webMappingWithAliases).isEqualTo(webMappingWithAliases); assertThat(webMappingWithPathAndValue).isEqualTo(webMappingWithPathAndValue); @@ -2122,51 +2070,33 @@ void equalsForSynthesizedAnnotations() throws Exception { @Test void hashCodeForSynthesizedAnnotations() throws Exception { - Method methodWithPath = WebController.class.getMethod( - "handleMappedWithPathAttribute"); - RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( - RequestMapping.class); + Method methodWithPath = WebController.class.getMethod("handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation(RequestMapping.class); assertThat(webMappingWithAliases).isNotNull(); - Method methodWithPathAndValue = WebController.class.getMethod( - "handleMappedWithSamePathAndValueAttributes"); - RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( - RequestMapping.class); + Method methodWithPathAndValue = WebController.class.getMethod("handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation(RequestMapping.class); assertThat(webMappingWithPathAndValue).isNotNull(); - RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( - webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from(webMappingWithAliases).synthesize(); assertThat(synthesizedWebMapping1).isNotNull(); - RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( - webMappingWithPathAndValue).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from(webMappingWithPathAndValue).synthesize(); assertThat(synthesizedWebMapping2).isNotNull(); // Equality amongst standard annotations - assertThat(webMappingWithAliases.hashCode()).isEqualTo( - webMappingWithAliases.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( - webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isEqualTo(webMappingWithAliases.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo(webMappingWithPathAndValue.hashCode()); // Inequality amongst standard annotations - assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( - webMappingWithPathAndValue.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isNotEqualTo( - webMappingWithAliases.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo(webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isNotEqualTo(webMappingWithAliases.hashCode()); // Equality amongst synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); - assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( - synthesizedWebMapping2.hashCode()); - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - synthesizedWebMapping2.hashCode()); - assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo(synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); // Equality between standard and synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( - webMappingWithPathAndValue.hashCode()); - assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo(webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo(synthesizedWebMapping1.hashCode()); // Inequality between standard and synthesized annotations - assertThat(synthesizedWebMapping1.hashCode()).isNotEqualTo( - webMappingWithAliases.hashCode()); - assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( - synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isNotEqualTo(webMappingWithAliases.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo(synthesizedWebMapping1.hashCode()); } /** @@ -2194,7 +2124,7 @@ void synthesizeNonPublicWithAttributeAliasesFromDifferentPackage() throws Except } @Test - void synthesizeWithArrayOfAnnotations() throws Exception { + void synthesizeWithArrayOfAnnotations() { Hierarchy hierarchy = HierarchyClass.class.getAnnotation(Hierarchy.class); assertThat(hierarchy).isNotNull(); Hierarchy synthesizedHierarchy = MergedAnnotation.from(hierarchy).synthesize(); @@ -2216,12 +2146,10 @@ void synthesizeWithArrayOfAnnotations() throws Exception { } @Test - void synthesizeWithArrayOfChars() throws Exception { - CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation( - CharsContainer.class); + void synthesizeWithArrayOfChars() { + CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation(CharsContainer.class); assertThat(charsContainer).isNotNull(); - CharsContainer synthesizedCharsContainer = MergedAnnotation.from( - charsContainer).synthesize(); + CharsContainer synthesizedCharsContainer = MergedAnnotation.from(charsContainer).synthesize(); assertSynthesized(synthesizedCharsContainer); char[] chars = synthesizedCharsContainer.chars(); assertThat(chars).containsExactly('x', 'y', 'z'); @@ -2234,53 +2162,50 @@ void synthesizeWithArrayOfChars() throws Exception { @Test void getValueWhenHasDefaultOverride() { - MergedAnnotation annotation = MergedAnnotations.from( - DefaultOverrideClass.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation = MergedAnnotations.from(DefaultOverrideClass.class) + .get(DefaultOverrideRoot.class); assertThat(annotation.getString("text")).isEqualTo("metameta"); } @Test // gh-22654 void getValueWhenHasDefaultOverrideWithImplicitAlias() { - MergedAnnotation annotation1 = MergedAnnotations.from( - DefaultOverrideImplicitAliasMetaClass1.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation1 = MergedAnnotations.from(DefaultOverrideImplicitAliasMetaClass1.class) + .get(DefaultOverrideRoot.class); assertThat(annotation1.getString("text")).isEqualTo("alias-meta-1"); - MergedAnnotation annotation2 = MergedAnnotations.from( - DefaultOverrideImplicitAliasMetaClass2.class).get(DefaultOverrideRoot.class); + MergedAnnotation annotation2 = MergedAnnotations.from(DefaultOverrideImplicitAliasMetaClass2.class) + .get(DefaultOverrideRoot.class); assertThat(annotation2.getString("text")).isEqualTo("alias-meta-2"); } @Test // gh-22654 void getValueWhenHasDefaultOverrideWithExplicitAlias() { - MergedAnnotation annotation = MergedAnnotations.from( - DefaultOverrideExplicitAliasRootMetaMetaClass.class).get( - DefaultOverrideExplicitAliasRoot.class); + MergedAnnotation annotation = MergedAnnotations.from(DefaultOverrideExplicitAliasRootMetaMetaClass.class) + .get(DefaultOverrideExplicitAliasRoot.class); assertThat(annotation.getString("text")).isEqualTo("meta"); assertThat(annotation.getString("value")).isEqualTo("meta"); } @Test // gh-22703 void getValueWhenThreeDeepMetaWithValue() { - MergedAnnotation annotation = MergedAnnotations.from( - ValueAttributeMetaMetaClass.class).get(ValueAttribute.class); - assertThat(annotation.getStringArray(MergedAnnotation.VALUE)).containsExactly( - "FromValueAttributeMeta"); + MergedAnnotation annotation = MergedAnnotations.from(ValueAttributeMetaMetaClass.class) + .get(ValueAttribute.class); + assertThat(annotation.getStringArray(MergedAnnotation.VALUE)).containsExactly("FromValueAttributeMeta"); } @Test void asAnnotationAttributesReturnsPopulatedAnnotationAttributes() { - MergedAnnotation annotation = MergedAnnotations.from( - SpringApplicationConfigurationClass.class).get( - SpringApplicationConfiguration.class); - AnnotationAttributes attributes = annotation.asAnnotationAttributes( - Adapt.CLASS_TO_STRING); - assertThat(attributes).containsEntry("classes", new String[] { Number.class.getName() }); + MergedAnnotation annotation = MergedAnnotations.from(SpringApplicationConfigurationClass.class) + .get(SpringApplicationConfiguration.class); + AnnotationAttributes attributes = annotation.asAnnotationAttributes(Adapt.CLASS_TO_STRING); + assertThat(attributes).containsEntry("classes", new String[] {Number.class.getName()}); assertThat(attributes.annotationType()).isEqualTo(SpringApplicationConfiguration.class); } + // @formatter:off + @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) - @Target({ ElementType.TYPE, ElementType.METHOD }) @Inherited @interface Transactional { @@ -2333,8 +2258,8 @@ static class ComposedTransactionalComponentClass { static class AliasedTransactionalComponentClass { } + @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) - @Target({ ElementType.TYPE, ElementType.METHOD }) @Inherited @interface AliasedTransactional { @@ -2701,7 +2626,7 @@ interface InterfaceWithInheritedAnnotation { void handleFromInterface(); } - static abstract class AbstractClassWithInheritedAnnotation + abstract static class AbstractClassWithInheritedAnnotation implements InterfaceWithInheritedAnnotation { @Transactional @@ -3005,7 +2930,7 @@ public void overrideWithoutNewAnnotation() { } } - public static abstract class SimpleGeneric { + public abstract static class SimpleGeneric { @Order(1) public abstract void something(T arg); @@ -3095,7 +3020,7 @@ public void foo(String t) { } } - public static abstract class BaseClassWithGenericAnnotatedMethod { + public abstract static class BaseClassWithGenericAnnotatedMethod { @Order abstract void foo(T t); From 1b4a4ac51f24682e4b605d452946fd12993b7c78 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 6 Jan 2024 23:21:57 +0100 Subject: [PATCH 052/261] Polishing --- .../core/annotation/MergedAnnotationsTests.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 6378c6f3a312..0958b66e2ea5 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -412,8 +412,7 @@ void collectMultiValueMapFromClassWithLocalComposedAnnotationAndInheritedAnnotat MultiValueMap map = MergedAnnotations.from( SubClassWithInheritedAnnotation.class, SearchStrategy.INHERITED_ANNOTATIONS) .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains( - entry("qualifier", Arrays.asList("composed2", "transactionManager"))); + assertThat(map).contains(entry("qualifier", List.of("composed2", "transactionManager"))); } @Test @@ -456,8 +455,7 @@ void collectMultiValueMapFromClassWithMultipleComposedAnnotations() { MultiValueMap map = MergedAnnotations.from( TxFromMultipleComposedAnnotations.class, SearchStrategy.INHERITED_ANNOTATIONS) .stream(Transactional.class).collect(MergedAnnotationCollectors.toMultiValueMap()); - assertThat(map).contains( - entry("value", Arrays.asList("TxInheritedComposed", "TxComposed"))); + assertThat(map).contains(entry("value", List.of("TxInheritedComposed", "TxComposed"))); } @Test @@ -656,7 +654,7 @@ void getWithTypeHierarchyFromSubSubNonInheritedAnnotationInterface() { @Test void getWithTypeHierarchyInheritedFromInterfaceMethod() throws Exception { Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handleFromInterface"); - MergedAnnotation annotation = MergedAnnotations.from(method,SearchStrategy.TYPE_HIERARCHY).get(Order.class); + MergedAnnotation annotation = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(Order.class); assertThat(annotation.isPresent()).isTrue(); assertThat(annotation.getAggregateIndex()).isEqualTo(1); } @@ -1130,7 +1128,7 @@ void getSuperClassSourceForTypesWithSingleCandidateType() { @Test void getSuperClassSourceForTypesWithMultipleCandidateTypes() { - List> candidates = Arrays.asList(Transactional.class, Order.class); + List> candidates = List.of(Transactional.class, Order.class); // no class-level annotation assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedInterface.class, candidates)).isNull(); From 867a1995073393506515ad91a4e5613c069a0b72 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 7 Jan 2024 00:12:15 +0100 Subject: [PATCH 053/261] Propagate arguments for dynamic prototype-scoped advice Closes gh-28407 (cherry picked from commit 43107e7eb18cdd2d02661421b5cdaf90e5c19c30) --- ...ntiationModelAwarePointcutAdvisorImpl.java | 4 +- .../annotation/ArgumentBindingTests.java | 49 ++++++++++++++----- .../beans/testfixture/beans/ITestBean.java | 6 ++- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java index 89a77213e080..07df51fb3f55 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -293,7 +293,7 @@ public boolean matches(Method method, Class targetClass) { @Override public boolean matches(Method method, Class targetClass, Object... args) { // This can match only on declared pointcut. - return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass)); + return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass, args)); } private boolean isAspectMaterialized() { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java index 4b1271816898..4bb8f1c984aa 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/ArgumentBindingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -42,26 +42,38 @@ */ class ArgumentBindingTests { + @Test + void annotationArgumentNameBinding() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TransactionalBean()); + proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); + + assertThatIllegalStateException() + .isThrownBy(proxiedTestBean::doInTransaction) + .withMessage("Invoked with @Transactional"); + } + @Test void bindingInPointcutUsedByAdvice() { AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); proxyFactory.addAspect(NamedPointcutWithArgs.class); - ITestBean proxiedTestBean = proxyFactory.getProxy(); + assertThatIllegalArgumentException() - .isThrownBy(() -> proxiedTestBean.setName("enigma")) - .withMessage("enigma"); + .isThrownBy(() -> proxiedTestBean.setName("enigma")) + .withMessage("enigma"); } @Test - void annotationArgumentNameBinding() { - AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TransactionalBean()); - proxyFactory.addAspect(PointcutWithAnnotationArgument.class); + void bindingWithDynamicAdvice() { + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(new TestBean()); + proxyFactory.addAspect(DynamicPointcutWithArgs.class); + ITestBean proxiedTestBean = proxyFactory.getProxy(); - ITransactionalBean proxiedTestBean = proxyFactory.getProxy(); - assertThatIllegalStateException() - .isThrownBy(proxiedTestBean::doInTransaction) - .withMessage("Invoked with @Transactional"); + proxiedTestBean.applyName(1); + assertThatIllegalArgumentException() + .isThrownBy(() -> proxiedTestBean.applyName("enigma")) + .withMessage("enigma"); } @Test @@ -94,6 +106,7 @@ public void doInTransaction() { } } + /** * Mimics Spring's @Transactional annotation without actually introducing the dependency. */ @@ -101,16 +114,17 @@ public void doInTransaction() { @interface Transactional { } + @Aspect static class PointcutWithAnnotationArgument { - @Around(value = "execution(* org.springframework..*.*(..)) && @annotation(transactional)") + @Around("execution(* org.springframework..*.*(..)) && @annotation(transactional)") public Object around(ProceedingJoinPoint pjp, Transactional transactional) throws Throwable { throw new IllegalStateException("Invoked with @Transactional"); } - } + @Aspect static class NamedPointcutWithArgs { @@ -121,7 +135,16 @@ public void pointcutWithArgs(String s) {} public Object doAround(ProceedingJoinPoint pjp, String aString) throws Throwable { throw new IllegalArgumentException(aString); } + } + + @Aspect("pertarget(execution(* *(..)))") + static class DynamicPointcutWithArgs { + + @Around("execution(* *(..)) && args(java.lang.String)") + public Object doAround(ProceedingJoinPoint pjp) throws Throwable { + throw new IllegalArgumentException(String.valueOf(pjp.getArgs()[0])); + } } } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java index 43ce2dd40ed8..1fa63057745d 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/ITestBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -33,6 +33,10 @@ public interface ITestBean extends AgeHolder { void setName(String name); + default void applyName(Object name) { + setName(String.valueOf(name)); + } + ITestBean getSpouse(); void setSpouse(ITestBean spouse); From bad01011da270cfc82dc922a0bcef6a420842e87 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 7 Jan 2024 16:33:49 +0100 Subject: [PATCH 054/261] Avoid getMostSpecificMethod resolution for non-annotated methods This is aligned with AutowiredAnnotationBeanPostProcessor now. Closes gh-31967 (cherry picked from commit 9912a52bb8d677a27aa79f0ebea146e6e0d120bb) --- .../CommonAnnotationBeanPostProcessor.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index 32d0cb9451dc..1de45e956e1c 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -387,8 +387,8 @@ else if (javaxResourceType != null && field.isAnnotationPresent(javaxResourceTyp if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { return; } - if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { - if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) { + if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@EJB annotation is not supported on static methods"); } @@ -398,7 +398,9 @@ else if (javaxResourceType != null && field.isAnnotationPresent(javaxResourceTyp PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); currElements.add(new EjbRefElement(method, bridgedMethod, pd)); } - else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) { + } + else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static methods"); } @@ -411,7 +413,9 @@ else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakart currElements.add(new ResourceElement(method, bridgedMethod, pd)); } } - else if (javaxResourceType != null && bridgedMethod.isAnnotationPresent(javaxResourceType)) { + } + else if (javaxResourceType != null && bridgedMethod.isAnnotationPresent(javaxResourceType)) { + if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static methods"); } From 8d51fc044484dc105da9da9d7c5a4631516567b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 4 Jan 2024 21:38:59 +0100 Subject: [PATCH 055/261] Add CORS support for Private Network Access This commit adds CORS support for Private Network Access by adding an Access-Control-Allow-Private-Network response header when the preflight request is sent with an Access-Control-Request-Private-Network header and that Private Network Access has been enabled in the CORS configuration. See https://developer.chrome.com/blog/private-network-access-preflight/ for more details. Closes gh-31975 (cherry picked from commit 318d4602564fb287b448ea310cf6c146f89cc47a) --- .../web/bind/annotation/CrossOrigin.java | 8 ++ .../web/cors/CorsConfiguration.java | 74 ++++++++++++++++-- .../web/cors/DefaultCorsProcessor.java | 17 ++++ .../cors/reactive/DefaultCorsProcessor.java | 17 ++++ .../web/cors/CorsConfigurationTests.java | 11 ++- .../web/cors/DefaultCorsProcessorTests.java | 71 +++++++++++++++++ .../reactive/DefaultCorsProcessorTests.java | 78 +++++++++++++++++++ .../web/reactive/config/CorsRegistration.java | 11 +++ .../handler/AbstractHandlerMapping.java | 1 + .../method/AbstractHandlerMethodMapping.java | 1 + .../RequestMappingHandlerMapping.java | 12 +++ .../reactive/config/CorsRegistryTests.java | 8 +- .../config/CorsBeanDefinitionParser.java | 1 + .../config/annotation/CorsRegistration.java | 11 +++ .../handler/AbstractHandlerMapping.java | 1 + .../handler/AbstractHandlerMethodMapping.java | 1 + .../RequestMappingHandlerMapping.java | 12 +++ .../config/annotation/CorsRegistryTests.java | 8 +- 18 files changed, 328 insertions(+), 15 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 5f28c1580c1b..8f5a1e7f421b 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 @@ -116,6 +116,14 @@ */ String allowCredentials() default ""; + /** + * Whether private network access is supported. Please, see + * {@link CorsConfiguration#setAllowPrivateNetwork(Boolean)} for details. + *

By default this is not set (i.e. private network access is not supported). + * @since 6.1.3 + */ + String allowPrivateNetwork() default ""; + /** * The maximum age (in seconds) of the cache duration for preflight responses. *

This property controls the value of the {@code Access-Control-Max-Age} 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 88ffe1bc96bd..4f09598cca0b 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 @@ -90,6 +90,9 @@ public class CorsConfiguration { @Nullable private Boolean allowCredentials; + @Nullable + private Boolean allowPrivateNetwork; + @Nullable private Long maxAge; @@ -114,6 +117,7 @@ public CorsConfiguration(CorsConfiguration other) { this.allowedHeaders = other.allowedHeaders; this.exposedHeaders = other.exposedHeaders; this.allowCredentials = other.allowCredentials; + this.allowPrivateNetwork = other.allowPrivateNetwork; this.maxAge = other.maxAge; } @@ -133,9 +137,10 @@ public CorsConfiguration(CorsConfiguration other) { * {@code Access-Control-Allow-Origin} response header is set either to the * matched domain value or to {@code "*"}. Keep in mind however that the * CORS spec does not allow {@code "*"} when {@link #setAllowCredentials - * allowCredentials} is set to {@code true} and as of 5.3 that combination - * is rejected in favor of using {@link #setAllowedOriginPatterns - * allowedOriginPatterns} instead. + * allowCredentials} is set to {@code true}, and does not recommend {@code "*"} + * when {@link #setAllowPrivateNetwork allowPrivateNetwork} is set to {@code true}. + * As a consequence, those combinations are rejected in favor of using + * {@link #setAllowedOriginPatterns allowedOriginPatterns} instead. *

By default this is not set which means that no origins are allowed. * However, an instance of this class is often initialized further, e.g. for * {@code @CrossOrigin}, via {@link #applyPermitDefaultValues()}. @@ -199,11 +204,13 @@ else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(th * note that such placeholders must be resolved externally. * *

In contrast to {@link #setAllowedOrigins(List) allowedOrigins} which - * only supports "*" and cannot be used with {@code allowCredentials}, when - * an allowedOriginPattern is matched, the {@code Access-Control-Allow-Origin} - * response header is set to the matched origin and not to {@code "*"} nor - * to the pattern. Therefore, allowedOriginPatterns can be used in combination - * with {@link #setAllowCredentials} set to {@code true}. + * only supports "*" and cannot be used with {@code allowCredentials} or + * {@code allowPrivateNetwork}, when an {@code allowedOriginPattern} is matched, + * the {@code Access-Control-Allow-Origin} response header is set to the + * matched origin and not to {@code "*"} nor to the pattern. + * Therefore, {@code allowedOriginPatterns} can be used in combination with + * {@link #setAllowCredentials} and {@link #setAllowPrivateNetwork} set to + * {@code true} *

By default this is not set. * @since 5.3 */ @@ -461,6 +468,33 @@ public Boolean getAllowCredentials() { return this.allowCredentials; } + /** + * Whether private network access is supported for user-agents restricting such access by default. + *

Private network requests are requests whose target server's IP address is more private than + * that from which the request initiator was fetched. For example, a request from a public website + * (https://example.com) to a private website (https://router.local), or a request from a private + * website to localhost. + *

Setting this property has an impact on how {@link #setAllowedOrigins(List) + * origins} and {@link #setAllowedOriginPatterns(List) originPatterns} are processed, + * see related API documentation for more details. + *

By default this is not set (i.e. private network access is not supported). + * @since 6.1.3 + * @see Private network access specifications + */ + public void setAllowPrivateNetwork(@Nullable Boolean allowPrivateNetwork) { + this.allowPrivateNetwork = allowPrivateNetwork; + } + + /** + * Return the configured {@code allowPrivateNetwork} flag, or {@code null} if none. + * @since 6.1.3 + * @see #setAllowPrivateNetwork(Boolean) + */ + @Nullable + public Boolean getAllowPrivateNetwork() { + return this.allowPrivateNetwork; + } + /** * Configure how long, as a duration, the response from a pre-flight request * can be cached by clients. @@ -543,6 +577,25 @@ public void validateAllowCredentials() { } } + /** + * Validate that when {@link #setAllowPrivateNetwork allowPrivateNetwork} is {@code true}, + * {@link #setAllowedOrigins allowedOrigins} does not contain the special + * value {@code "*"} since this is insecure. + * @throws IllegalArgumentException if the validation fails + * @since 6.1.3 + */ + public void validateAllowPrivateNetwork() { + if (this.allowPrivateNetwork == Boolean.TRUE && + this.allowedOrigins != null && this.allowedOrigins.contains(ALL)) { + + throw new IllegalArgumentException( + "When allowPrivateNetwork is true, allowedOrigins cannot contain the special value \"*\" " + + "as it is not recommended from a security perspective. " + + "To allow private network access to a set of origins, list them explicitly " + + "or consider using \"allowedOriginPatterns\" instead."); + } + } + /** * Combine the non-null properties of the supplied * {@code CorsConfiguration} with this one. @@ -577,6 +630,10 @@ public CorsConfiguration combine(@Nullable CorsConfiguration other) { if (allowCredentials != null) { config.setAllowCredentials(allowCredentials); } + Boolean allowPrivateNetwork = other.getAllowPrivateNetwork(); + if (allowPrivateNetwork != null) { + config.setAllowPrivateNetwork(allowPrivateNetwork); + } Long maxAge = other.getMaxAge(); if (maxAge != null) { config.setMaxAge(maxAge); @@ -640,6 +697,7 @@ public String checkOrigin(@Nullable String origin) { if (!ObjectUtils.isEmpty(this.allowedOrigins)) { if (this.allowedOrigins.contains(ALL)) { validateAllowCredentials(); + validateAllowPrivateNetwork(); return ALL; } for (String allowedOrigin : this.allowedOrigins) { diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java index 177d28b5a63d..c134d806df9b 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java @@ -54,6 +54,18 @@ public class DefaultCorsProcessor implements CorsProcessor { private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class); + /** + * The {@code Access-Control-Request-Private-Network} request header field name. + * @see Private Network Access specification + */ + static final String ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK = "Access-Control-Request-Private-Network"; + + /** + * The {@code Access-Control-Allow-Private-Network} response header field name. + * @see Private Network Access specification + */ + static final String ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network"; + @Override @SuppressWarnings("resource") @@ -155,6 +167,11 @@ protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse r responseHeaders.setAccessControlAllowCredentials(true); } + if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) && + Boolean.parseBoolean(request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK))) { + responseHeaders.set(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, Boolean.toString(true)); + } + if (preFlightRequest && config.getMaxAge() != null) { responseHeaders.setAccessControlMaxAge(config.getMaxAge()); } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java index 8f6b16f1f7b4..a259efbb8e74 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java @@ -52,6 +52,18 @@ public class DefaultCorsProcessor implements CorsProcessor { private static final List VARY_HEADERS = List.of( HttpHeaders.ORIGIN, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + /** + * The {@code Access-Control-Request-Private-Network} request header field name. + * @see Private Network Access specification + */ + static final String ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK = "Access-Control-Request-Private-Network"; + + /** + * The {@code Access-Control-Allow-Private-Network} response header field name. + * @see Private Network Access specification + */ + static final String ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network"; + @Override public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) { @@ -153,6 +165,11 @@ protected boolean handleInternal(ServerWebExchange exchange, responseHeaders.setAccessControlAllowCredentials(true); } + if (Boolean.TRUE.equals(config.getAllowPrivateNetwork()) && + Boolean.parseBoolean(request.getHeaders().getFirst(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK))) { + responseHeaders.set(ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK, Boolean.toString(true)); + } + if (preFlightRequest && config.getMaxAge() != null) { responseHeaders.setAccessControlMaxAge(config.getMaxAge()); } 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 567240558273..01c925488fa8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -50,6 +50,8 @@ void setNullValues() { assertThat(config.getExposedHeaders()).isNull(); config.setAllowCredentials(null); assertThat(config.getAllowCredentials()).isNull(); + config.setAllowPrivateNetwork(null); + assertThat(config.getAllowPrivateNetwork()).isNull(); config.setMaxAge((Long) null); assertThat(config.getMaxAge()).isNull(); } @@ -63,6 +65,7 @@ void setValues() { config.addAllowedMethod("*"); config.addExposedHeader("*"); config.setAllowCredentials(true); + config.setAllowPrivateNetwork(true); config.setMaxAge(123L); assertThat(config.getAllowedOrigins()).containsExactly("*"); @@ -71,6 +74,7 @@ void setValues() { assertThat(config.getAllowedMethods()).containsExactly("*"); assertThat(config.getExposedHeaders()).containsExactly("*"); assertThat(config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowPrivateNetwork()).isTrue(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123)); } @@ -93,6 +97,7 @@ void combineWithNullProperties() { config.addAllowedMethod(HttpMethod.GET.name()); config.setMaxAge(123L); config.setAllowCredentials(true); + config.setAllowPrivateNetwork(true); CorsConfiguration other = new CorsConfiguration(); config = config.combine(other); @@ -105,6 +110,7 @@ void combineWithNullProperties() { assertThat(config.getAllowedMethods()).containsExactly(HttpMethod.GET.name()); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(123)); assertThat(config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowPrivateNetwork()).isTrue(); } @Test // SPR-15772 @@ -258,6 +264,7 @@ void combine() { config.addAllowedMethod(HttpMethod.GET.name()); config.setMaxAge(123L); config.setAllowCredentials(true); + config.setAllowPrivateNetwork(true); CorsConfiguration other = new CorsConfiguration(); other.addAllowedOrigin("https://domain2.com"); @@ -267,6 +274,7 @@ void combine() { other.addAllowedMethod(HttpMethod.PUT.name()); other.setMaxAge(456L); other.setAllowCredentials(false); + other.setAllowPrivateNetwork(false); config = config.combine(other); assertThat(config).isNotNull(); @@ -277,6 +285,7 @@ void combine() { assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(456)); assertThat(config).isNotNull(); assertThat(config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowPrivateNetwork()).isFalse(); assertThat(config.getAllowedOriginPatterns()).containsExactly("http://*.domain1.com", "http://*.domain2.com"); } diff --git a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java index 735504aa204a..2bd442867cfb 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/DefaultCorsProcessorTests.java @@ -351,6 +351,32 @@ public void preflightRequestCredentialsWithWildcardOrigin() throws Exception { assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); } + @Test + public void preflightRequestPrivateNetworkWithWildcardOrigin() throws Exception { + this.request.setMethod(HttpMethod.OPTIONS.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "Header1"); + this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"); + this.conf.setAllowedOrigins(Arrays.asList("https://domain1.com", "*", "http://domain3.example")); + this.conf.addAllowedHeader("Header1"); + this.conf.setAllowPrivateNetwork(true); + + assertThatIllegalArgumentException().isThrownBy(() -> + this.processor.processRequest(this.conf, this.request, this.response)); + + this.conf.setAllowedOrigins(null); + this.conf.addAllowedOriginPattern("*"); + + this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(this.response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); + assertThat(this.response.getHeaders(HttpHeaders.VARY)).contains(HttpHeaders.ORIGIN, + HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + @Test public void preflightRequestAllowedHeaders() throws Exception { this.request.setMethod(HttpMethod.OPTIONS.name()); @@ -434,4 +460,49 @@ public void preventDuplicatedVaryHeaders() throws Exception { HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); } + @Test + public void preflightRequestWithoutAccessControlRequestPrivateNetwork() throws Exception { + this.request.setMethod(HttpMethod.OPTIONS.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + @Test + public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() throws Exception { + this.request.setMethod(HttpMethod.OPTIONS.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"); + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + @Test + public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() throws Exception { + this.request.setMethod(HttpMethod.OPTIONS.name()); + this.request.addHeader(HttpHeaders.ORIGIN, "https://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + this.request.addHeader(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true"); + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + this.conf.setAllowPrivateNetwork(true); + + this.processor.processRequest(this.conf, this.request, this.response); + assertThat(this.response.containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(this.response.containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index eb7a97ecab0f..87ee906f86e6 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -364,6 +364,33 @@ public void preflightRequestCredentialsWithWildcardOrigin() { assertThat((Object) response.getStatusCode()).isNull(); } + @Test + public void preflightRequestPrivateNetworkWithWildcardOrigin() { + ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() + .header(ACCESS_CONTROL_REQUEST_METHOD, "GET") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "Header1") + .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true")); + + this.conf.addAllowedOrigin("https://domain1.com"); + this.conf.addAllowedOrigin("*"); + this.conf.addAllowedOrigin("http://domain3.example"); + this.conf.addAllowedHeader("Header1"); + this.conf.setAllowPrivateNetwork(true); + assertThatIllegalArgumentException().isThrownBy(() -> this.processor.process(this.conf, exchange)); + + this.conf.setAllowedOrigins(null); + this.conf.addAllowedOriginPattern("*"); + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); + assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, + ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); + assertThat(response.getStatusCode()).isNull(); + } + @Test public void preflightRequestAllowedHeaders() { ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() @@ -460,6 +487,57 @@ public void preventDuplicatedVaryHeaders() { ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); } + @Test + public void preflightRequestWithoutAccessControlRequestPrivateNetwork() { + ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() + .header(ACCESS_CONTROL_REQUEST_METHOD, "GET")); + + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(response.getStatusCode()).isNull(); + } + + @Test + public void preflightRequestWithAccessControlRequestPrivateNetworkNotAllowed() { + ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() + .header(ACCESS_CONTROL_REQUEST_METHOD, "GET") + .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true")); + + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(response.getStatusCode()).isNull(); + } + + @Test + public void preflightRequestWithAccessControlRequestPrivateNetworkAllowed() { + ServerWebExchange exchange = MockServerWebExchange.from(preFlightRequest() + .header(ACCESS_CONTROL_REQUEST_METHOD, "GET") + .header(DefaultCorsProcessor.ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK, "true")); + + this.conf.addAllowedHeader("*"); + this.conf.addAllowedOrigin("https://domain2.com"); + this.conf.setAllowPrivateNetwork(true); + + this.processor.process(this.conf, exchange); + + ServerHttpResponse response = exchange.getResponse(); + assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(response.getStatusCode()).isNull(); + } + private ServerWebExchange actualRequest() { return MockServerWebExchange.from(corsRequest(HttpMethod.GET)); 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 383505c4c7fa..32c15d439914 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 @@ -131,6 +131,17 @@ public CorsRegistration allowCredentials(boolean allowCredentials) { return this; } + /** + * Whether private network access is supported. + *

Please, see {@link CorsConfiguration#setAllowPrivateNetwork(Boolean)} for details. + *

By default this is not set (i.e. private network access is not supported). + * @since 6.1.3 + */ + public CorsRegistration allowPrivateNetwork(boolean allowPrivateNetwork) { + this.config.setAllowPrivateNetwork(allowPrivateNetwork); + return this; + } + /** * Configure how long in seconds the response from a pre-flight request * can be cached by clients. diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java index 391695382b4f..de09dba809c9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -196,6 +196,7 @@ public Mono getHandler(ServerWebExchange exchange) { config = (config != null ? config.combine(handlerConfig) : handlerConfig); if (config != null) { config.validateAllowCredentials(); + config.validateAllowPrivateNetwork(); } if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) { return NO_OP_HANDLER; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java index 448557c77001..237b36c7d2d2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java @@ -532,6 +532,7 @@ public void register(T mapping, Object handler, Method method) { CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { corsConfig.validateAllowCredentials(); + corsConfig.validateAllowPrivateNetwork(); this.corsLookup.put(handlerMethod, corsConfig); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java index d05339ef8e5c..02882352a816 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java @@ -343,6 +343,18 @@ else if (!allowCredentials.isEmpty()) { "or an empty string (\"\"): current value is [" + allowCredentials + "]"); } + String allowPrivateNetwork = resolveCorsAnnotationValue(annotation.allowPrivateNetwork()); + if ("true".equalsIgnoreCase(allowPrivateNetwork)) { + config.setAllowPrivateNetwork(true); + } + else if ("false".equalsIgnoreCase(allowPrivateNetwork)) { + config.setAllowPrivateNetwork(false); + } + else if (!allowPrivateNetwork.isEmpty()) { + throw new IllegalStateException("@CrossOrigin's allowPrivateNetwork value must be \"true\", \"false\", " + + "or an empty string (\"\"): current value is [" + allowPrivateNetwork + "]"); + } + if (annotation.maxAge() >= 0) { config.setMaxAge(annotation.maxAge()); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java index f03254e7bff3..355a3b319de3 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/CorsRegistryTests.java @@ -51,8 +51,8 @@ public void multipleMappings() { @Test public void customizedMapping() { this.registry.addMapping("/foo").allowedOrigins("https://domain2.com", "https://domain2.com") - .allowedMethods("DELETE").allowCredentials(false).allowedHeaders("header1", "header2") - .exposedHeaders("header3", "header4").maxAge(3600); + .allowedMethods("DELETE").allowCredentials(true).allowPrivateNetwork(true) + .allowedHeaders("header1", "header2").exposedHeaders("header3", "header4").maxAge(3600); Map configs = this.registry.getCorsConfigurations(); assertThat(configs).hasSize(1); CorsConfiguration config = configs.get("/foo"); @@ -60,7 +60,8 @@ public void customizedMapping() { assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("DELETE")); assertThat(config.getAllowedHeaders()).isEqualTo(Arrays.asList("header1", "header2")); assertThat(config.getExposedHeaders()).isEqualTo(Arrays.asList("header3", "header4")); - assertThat(config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowPrivateNetwork()).isTrue(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(3600)); } @@ -90,6 +91,7 @@ void combine() { assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); assertThat(config.getExposedHeaders()).isEmpty(); assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getAllowPrivateNetwork()).isNull(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java index 20997963fe6f..62e4b553f592 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java @@ -84,6 +84,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } config.applyPermitDefaultValues(); config.validateAllowCredentials(); + config.validateAllowPrivateNetwork(); corsConfigurations.put(mapping.getAttribute("path"), config); } } 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 e1a25396c7cf..cd37917349a2 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 @@ -132,6 +132,17 @@ public CorsRegistration allowCredentials(boolean allowCredentials) { return this; } + /** + * Whether private network access is supported. + *

By default this is not set (i.e. private network access is not supported). + * @since 6.1.3 + * @see Private network access specifications + */ + public CorsRegistration allowPrivateNetwork(boolean allowPrivateNetwork) { + this.config.setAllowPrivateNetwork(allowPrivateNetwork); + return this; + } + /** * Configure how long in seconds the response from a pre-flight request * can be cached by clients. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index e0a56b82a7b8..12d1886f8131 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -536,6 +536,7 @@ else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDisp } if (config != null) { config.validateAllowCredentials(); + config.validateAllowPrivateNetwork(); } executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index 85d61f2ce6aa..f9f5c68e429c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -646,6 +646,7 @@ public void register(T mapping, Object handler, Method method) { CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { corsConfig.validateAllowCredentials(); + corsConfig.validateAllowPrivateNetwork(); this.corsLookup.put(handlerMethod, corsConfig); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index d30fcc6a3ad3..43a13d34670b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -522,6 +522,18 @@ else if (!allowCredentials.isEmpty()) { "or an empty string (\"\"): current value is [" + allowCredentials + "]"); } + String allowPrivateNetwork = resolveCorsAnnotationValue(annotation.allowPrivateNetwork()); + if ("true".equalsIgnoreCase(allowPrivateNetwork)) { + config.setAllowPrivateNetwork(true); + } + else if ("false".equalsIgnoreCase(allowPrivateNetwork)) { + config.setAllowPrivateNetwork(false); + } + else if (!allowPrivateNetwork.isEmpty()) { + throw new IllegalStateException("@CrossOrigin's allowPrivateNetwork value must be \"true\", \"false\", " + + "or an empty string (\"\"): current value is [" + allowPrivateNetwork + "]"); + } + if (annotation.maxAge() >= 0 ) { config.setMaxAge(annotation.maxAge()); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java index 9b267eb814a7..42c5d7fe1e17 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CorsRegistryTests.java @@ -56,8 +56,8 @@ public void multipleMappings() { @Test public void customizedMapping() { this.registry.addMapping("/foo").allowedOrigins("https://domain2.com", "https://domain2.com") - .allowedMethods("DELETE").allowCredentials(false).allowedHeaders("header1", "header2") - .exposedHeaders("header3", "header4").maxAge(3600); + .allowedMethods("DELETE").allowCredentials(true).allowPrivateNetwork(true) + .allowedHeaders("header1", "header2").exposedHeaders("header3", "header4").maxAge(3600); Map configs = this.registry.getCorsConfigurations(); assertThat(configs).hasSize(1); CorsConfiguration config = configs.get("/foo"); @@ -65,7 +65,8 @@ public void customizedMapping() { assertThat(config.getAllowedMethods()).isEqualTo(Collections.singletonList("DELETE")); assertThat(config.getAllowedHeaders()).isEqualTo(Arrays.asList("header1", "header2")); assertThat(config.getExposedHeaders()).isEqualTo(Arrays.asList("header3", "header4")); - assertThat(config.getAllowCredentials()).isFalse(); + assertThat(config.getAllowCredentials()).isTrue(); + assertThat(config.getAllowPrivateNetwork()).isTrue(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(3600)); } @@ -95,6 +96,7 @@ void combine() { assertThat(config.getAllowedHeaders()).isEqualTo(Collections.singletonList("*")); assertThat(config.getExposedHeaders()).isEmpty(); assertThat(config.getAllowCredentials()).isNull(); + assertThat(config.getAllowPrivateNetwork()).isNull(); assertThat(config.getMaxAge()).isEqualTo(Long.valueOf(1800)); } } From 0c6957e39592817934980603f00ef45e59666d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 8 Jan 2024 12:26:03 +0100 Subject: [PATCH 056/261] Polishing See gh-31975 --- .../java/org/springframework/web/cors/CorsConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4f09598cca0b..26c9592b2db2 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 @@ -210,7 +210,7 @@ else if (this.allowedOrigins == DEFAULT_PERMIT_ALL && CollectionUtils.isEmpty(th * matched origin and not to {@code "*"} nor to the pattern. * Therefore, {@code allowedOriginPatterns} can be used in combination with * {@link #setAllowCredentials} and {@link #setAllowPrivateNetwork} set to - * {@code true} + * {@code true}. *

By default this is not set. * @since 5.3 */ From 0c22866b720469f7d45022ddd1a171ceffa15690 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 8 Jan 2024 14:45:31 +0100 Subject: [PATCH 057/261] Ensure correct capacity in DefaultDataBuffer See gh-31873 Closes gh-31979 --- .../core/io/buffer/DataBuffer.java | 32 +++++---- .../core/io/buffer/DefaultDataBuffer.java | 11 ++- .../core/io/buffer/DataBufferTests.java | 68 +++++++++++++++++-- 3 files changed, 87 insertions(+), 24 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java index b4360235179f..94521587035e 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -263,29 +263,35 @@ default DataBuffer ensureCapacity(int capacity) { default DataBuffer write(CharSequence charSequence, Charset charset) { Assert.notNull(charSequence, "CharSequence must not be null"); Assert.notNull(charset, "Charset must not be null"); - if (charSequence.length() > 0) { + if (!charSequence.isEmpty()) { CharsetEncoder encoder = charset.newEncoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE); CharBuffer src = CharBuffer.wrap(charSequence); - int cap = (int) (src.remaining() * encoder.averageBytesPerChar()); + int averageSize = (int) Math.ceil(src.remaining() * encoder.averageBytesPerChar()); + ensureWritable(averageSize); while (true) { - ensureWritable(cap); CoderResult cr; - try (ByteBufferIterator iterator = writableByteBuffers()) { - Assert.state(iterator.hasNext(), "No ByteBuffer available"); - ByteBuffer dest = iterator.next(); - cr = encoder.encode(src, dest, true); - if (cr.isUnderflow()) { - cr = encoder.flush(dest); + if (src.hasRemaining()) { + try (ByteBufferIterator iterator = writableByteBuffers()) { + Assert.state(iterator.hasNext(), "No ByteBuffer available"); + ByteBuffer dest = iterator.next(); + cr = encoder.encode(src, dest, true); + if (cr.isUnderflow()) { + cr = encoder.flush(dest); + } + writePosition(writePosition() + dest.position()); } - writePosition(dest.position()); + } + else { + cr = CoderResult.UNDERFLOW; } if (cr.isUnderflow()) { break; } - if (cr.isOverflow()) { - cap = 2 * cap + 1; + else if (cr.isOverflow()) { + int maxSize = (int) Math.ceil(src.remaining() * encoder.maxBytesPerChar()); + ensureWritable(maxSize); } } } diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java index 4456f975bd9e..8d9fcda64354 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DefaultDataBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -416,16 +416,15 @@ public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { @Override public DataBuffer.ByteBufferIterator readableByteBuffers() { - ByteBuffer readOnly = this.byteBuffer.asReadOnlyBuffer(); - readOnly.clear().position(this.readPosition).limit(this.writePosition - this.readPosition); + ByteBuffer readOnly = this.byteBuffer.slice(this.readPosition, readableByteCount()) + .asReadOnlyBuffer(); return new ByteBufferIterator(readOnly); } @Override public DataBuffer.ByteBufferIterator writableByteBuffers() { - ByteBuffer duplicate = this.byteBuffer.duplicate(); - duplicate.clear().position(this.writePosition).limit(this.capacity - this.writePosition); - return new ByteBufferIterator(duplicate); + ByteBuffer slice = this.byteBuffer.slice(this.writePosition, writableByteCount()); + return new ByteBufferIterator(slice); } @Override diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java index cd3dd3c57986..2731ac7d1e37 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -678,6 +678,38 @@ void toByteBufferDestination(DataBufferFactory bufferFactory) { void readableByteBuffers(DataBufferFactory bufferFactory) throws IOException { super.bufferFactory = bufferFactory; + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(3); + dataBuffer.write("abc".getBytes(StandardCharsets.UTF_8)); + dataBuffer.readPosition(1); + dataBuffer.writePosition(2); + + + byte[] result = new byte[1]; + try (var iterator = dataBuffer.readableByteBuffers()) { + assertThat(iterator).hasNext(); + int i = 0; + while (iterator.hasNext()) { + ByteBuffer byteBuffer = iterator.next(); + assertThat(byteBuffer.position()).isEqualTo(0); + assertThat(byteBuffer.limit()).isEqualTo(1); + assertThat(byteBuffer.capacity()).isEqualTo(1); + assertThat(byteBuffer.remaining()).isEqualTo(1); + + byteBuffer.get(result, i, 1); + + assertThat(iterator).isExhausted(); + } + } + + assertThat(result).containsExactly('b'); + + release(dataBuffer); + } + + @ParameterizedDataBufferAllocatingTest + void readableByteBuffersJoined(DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + DataBuffer dataBuffer = this.bufferFactory.join(Arrays.asList(stringBuffer("a"), stringBuffer("b"), stringBuffer("c"))); @@ -703,17 +735,26 @@ void readableByteBuffers(DataBufferFactory bufferFactory) throws IOException { void writableByteBuffers(DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; - DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(1); + DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(3); + dataBuffer.write("ab".getBytes(StandardCharsets.UTF_8)); + dataBuffer.readPosition(1); try (DataBuffer.ByteBufferIterator iterator = dataBuffer.writableByteBuffers()) { assertThat(iterator).hasNext(); ByteBuffer byteBuffer = iterator.next(); - byteBuffer.put((byte) 'a'); - dataBuffer.writePosition(1); + assertThat(byteBuffer.position()).isEqualTo(0); + assertThat(byteBuffer.limit()).isEqualTo(1); + assertThat(byteBuffer.capacity()).isEqualTo(1); + assertThat(byteBuffer.remaining()).isEqualTo(1); + + byteBuffer.put((byte) 'c'); + dataBuffer.writePosition(3); assertThat(iterator).isExhausted(); } - assertThat(dataBuffer.read()).isEqualTo((byte) 'a'); + byte[] result = new byte[2]; + dataBuffer.read(result); + assertThat(result).containsExactly('b', 'c'); release(dataBuffer); } @@ -945,4 +986,21 @@ void shouldHonorSourceBuffersReadPosition(DataBufferFactory bufferFactory) { assertThat(StandardCharsets.UTF_8.decode(byteBuffer).toString()).isEqualTo("b"); } + @ParameterizedDataBufferAllocatingTest // gh-31873 + void repeatedWrites(DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer buffer = bufferFactory.allocateBuffer(256); + String name = "Müller"; + int repeatCount = 19; + for (int i = 0; i < repeatCount; i++) { + buffer.write(name, StandardCharsets.UTF_8); + } + String result = buffer.toString(StandardCharsets.UTF_8); + String expected = name.repeat(repeatCount); + assertThat(result).isEqualTo(expected); + + release(buffer); + } + } From c44bb29aa5a48b6594ca237d05274e5b85413938 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 9 Jan 2024 12:56:52 +0100 Subject: [PATCH 058/261] Polishing --- .../aspectj/AspectJExpressionPointcut.java | 12 ++--- .../AspectJExpressionPointcutTests.java | 48 +++++++++---------- .../support/AbstractApplicationContext.java | 3 +- .../AspectJAutoProxyCreatorTests.java | 40 +++++----------- .../jdbc/core/JdbcTemplate.java | 4 +- ...LErrorCodeSQLExceptionTranslatorTests.java | 29 +++++------ 6 files changed, 58 insertions(+), 78 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index c6ee43b2ec26..51463440472c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -333,12 +333,12 @@ public boolean matches(Method method, Class targetClass, Object... args) { Object targetObject = null; Object thisObject = null; try { - MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation(); - targetObject = mi.getThis(); - if (!(mi instanceof ProxyMethodInvocation _pmi)) { - throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); + MethodInvocation curr = ExposeInvocationInterceptor.currentInvocation(); + targetObject = curr.getThis(); + if (!(curr instanceof ProxyMethodInvocation currPmi)) { + throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + curr); } - pmi = _pmi; + pmi = currPmi; thisObject = pmi.getProxy(); } catch (IllegalStateException ex) { diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index e1b2a93b9589..0a2b5ab8f9ba 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -23,7 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.aspectj.weaver.tools.PointcutExpression; import org.aspectj.weaver.tools.PointcutPrimitive; import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.junit.jupiter.api.BeforeEach; @@ -65,7 +64,7 @@ public class AspectJExpressionPointcutTests { @BeforeEach - public void setUp() throws NoSuchMethodException { + public void setup() throws NoSuchMethodException { getAge = TestBean.class.getMethod("getAge"); setAge = TestBean.class.getMethod("setAge", int.class); setSomeNumber = TestBean.class.getMethod("setSomeNumber", Number.class); @@ -246,14 +245,13 @@ public void testDynamicMatchingProxy() { @Test public void testInvalidExpression() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)"; - assertThatIllegalArgumentException().isThrownBy( - getPointcut(expression)::getClassFilter); // call to getClassFilter forces resolution + assertThatIllegalArgumentException().isThrownBy(getPointcut(expression)::getClassFilter); // call to getClassFilter forces resolution } private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) { TestBean target = new TestBean(); - Pointcut pointcut = getPointcut(pointcutExpression); + AspectJExpressionPointcut pointcut = getPointcut(pointcutExpression); DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(interceptor); @@ -277,40 +275,31 @@ private void assertMatchesTestBeanClass(ClassFilter classFilter) { @Test public void testWithUnsupportedPointcutPrimitive() { String expression = "call(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; - assertThatExceptionOfType(UnsupportedPointcutPrimitiveException.class).isThrownBy(() -> - getPointcut(expression).getClassFilter()) // call to getClassFilter forces resolution... - .satisfies(ex -> assertThat(ex.getUnsupportedPrimitive()).isEqualTo(PointcutPrimitive.CALL)); + assertThatExceptionOfType(UnsupportedPointcutPrimitiveException.class) + .isThrownBy(() -> getPointcut(expression).getClassFilter()) // call to getClassFilter forces resolution... + .satisfies(ex -> assertThat(ex.getUnsupportedPrimitive()).isEqualTo(PointcutPrimitive.CALL)); } @Test public void testAndSubstitution() { - Pointcut pc = getPointcut("execution(* *(..)) and args(String)"); - PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); - assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String)"); + AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String)"); + String expr = pc.getPointcutExpression().getPointcutExpression(); + assertThat(expr).isEqualTo("execution(* *(..)) && args(String)"); } @Test public void testMultipleAndSubstitutions() { - Pointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); - PointcutExpression expr = ((AspectJExpressionPointcut) pc).getPointcutExpression(); - assertThat(expr.getPointcutExpression()).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); + AspectJExpressionPointcut pc = getPointcut("execution(* *(..)) and args(String) and this(Object)"); + String expr = pc.getPointcutExpression().getPointcutExpression(); + assertThat(expr).isEqualTo("execution(* *(..)) && args(String) && this(Object)"); } - private Pointcut getPointcut(String expression) { + private AspectJExpressionPointcut getPointcut(String expression) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression(expression); return pointcut; } - - public static class OtherIOther implements IOther { - - @Override - public void absquatulate() { - // Empty - } - } - @Test public void testMatchGenericArgument() { String expression = "execution(* set*(java.util.List) )"; @@ -505,6 +494,15 @@ public void testAnnotationOnMethodArgumentsWithWildcards() throws Exception { } + public static class OtherIOther implements IOther { + + @Override + public void absquatulate() { + // Empty + } + } + + public static class HasGeneric { public void setFriends(List friends) { 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 e87311ecbad2..548c752390a0 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -595,7 +595,6 @@ public void refresh() throws BeansException, IllegalStateException { StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process"); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); - // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); beanPostProcess.end(); diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java index 69f5f4bd888e..64f670650ca8 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -220,16 +220,16 @@ void cglibProxyClassIsCachedAcrossApplicationContextsForPerTargetAspect() { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) { testBean1 = context.getBean(TestBean.class); assertThat(AopUtils.isCglibProxy(testBean1)).as("CGLIB proxy").isTrue(); - assertThat(testBean1.getClass().getInterfaces()) - .containsExactlyInAnyOrder(Factory.class, SpringProxy.class, Advised.class); + assertThat(testBean1.getClass().getInterfaces()).containsExactlyInAnyOrder( + Factory.class, SpringProxy.class, Advised.class); } // Round #2 try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(configClass)) { testBean2 = context.getBean(TestBean.class); assertThat(AopUtils.isCglibProxy(testBean2)).as("CGLIB proxy").isTrue(); - assertThat(testBean2.getClass().getInterfaces()) - .containsExactlyInAnyOrder(Factory.class, SpringProxy.class, Advised.class); + assertThat(testBean2.getClass().getInterfaces()).containsExactlyInAnyOrder( + Factory.class, SpringProxy.class, Advised.class); } assertThat(testBean1.getClass()).isSameAs(testBean2.getClass()); @@ -344,8 +344,8 @@ void lambdaIsAlwaysProxiedWithJdkProxy(Class configClass) { Supplier supplier = context.getBean(Supplier.class); assertThat(AopUtils.isAopProxy(supplier)).as("AOP proxy").isTrue(); assertThat(AopUtils.isJdkDynamicProxy(supplier)).as("JDK Dynamic proxy").isTrue(); - assertThat(supplier.getClass().getInterfaces()) - .containsExactlyInAnyOrder(Supplier.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + assertThat(supplier.getClass().getInterfaces()).containsExactlyInAnyOrder( + Supplier.class, SpringProxy.class, Advised.class, DecoratingProxy.class); assertThat(supplier.get()).isEqualTo("advised: lambda"); } } @@ -357,26 +357,14 @@ void lambdaIsAlwaysProxiedWithJdkProxyWithIntroductions(Class configClass) { MessageGenerator messageGenerator = context.getBean(MessageGenerator.class); assertThat(AopUtils.isAopProxy(messageGenerator)).as("AOP proxy").isTrue(); assertThat(AopUtils.isJdkDynamicProxy(messageGenerator)).as("JDK Dynamic proxy").isTrue(); - assertThat(messageGenerator.getClass().getInterfaces()) - .containsExactlyInAnyOrder(MessageGenerator.class, Mixin.class, SpringProxy.class, Advised.class, DecoratingProxy.class); + assertThat(messageGenerator.getClass().getInterfaces()).containsExactlyInAnyOrder( + MessageGenerator.class, Mixin.class, SpringProxy.class, Advised.class, DecoratingProxy.class); assertThat(messageGenerator.generateMessage()).isEqualTo("mixin: lambda"); } } - /** - * Returns a new {@link ClassPathXmlApplicationContext} for the file ending in fileSuffix. - */ private ClassPathXmlApplicationContext newContext(String fileSuffix) { - return new ClassPathXmlApplicationContext(qName(fileSuffix), getClass()); - } - - /** - * Returns the relatively qualified name for fileSuffix. - * e.g. for a fileSuffix='foo.xml', this method will return - * 'AspectJAutoProxyCreatorTests-foo.xml' - */ - private String qName(String fileSuffix) { - return String.format("%s-%s", getClass().getSimpleName(), fileSuffix); + return new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-" + fileSuffix, getClass()); } } @@ -416,7 +404,6 @@ class DummyAspectWithParameter { public Object test(ProceedingJoinPoint pjp, int age) throws Throwable { return pjp.proceed(); } - } class DummyFactoryBean implements FactoryBean { @@ -435,7 +422,6 @@ public Class getObjectType() { public boolean isSingleton() { throw new UnsupportedOperationException(); } - } @Aspect @@ -591,7 +577,6 @@ public int unreliable() { } return this.calls; } - } @SuppressWarnings("serial") @@ -607,7 +592,6 @@ public TestBeanAdvisor() { public boolean matches(Method method, @Nullable Class targetClass) { return ITestBean.class.isAssignableFrom(targetClass); } - } abstract class AbstractProxyTargetClassConfig { @@ -661,6 +645,7 @@ PerTargetAspect perTargetAspect() { @FunctionalInterface interface MessageGenerator { + String generateMessage(); } @@ -678,7 +663,6 @@ public Object invoke(MethodInvocation invocation) throws Throwable { public boolean implementsInterface(Class intf) { return Mixin.class.isAssignableFrom(intf); } - } @SuppressWarnings("serial") @@ -708,7 +692,6 @@ public ClassFilter getClassFilter() { public void validateInterfaces() { /* no-op */ } - } abstract class AbstractMixinConfig { @@ -722,7 +705,6 @@ MessageGenerator messageGenerator() { MixinAdvisor mixinAdvisor() { return new MixinAdvisor(); } - } @Configuration(proxyBeanMethods = false) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index a03381c59814..14d1c94e0501 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -1177,7 +1177,7 @@ public T execute(CallableStatementCreator csc, CallableStatementCallback Assert.notNull(action, "Callback object must not be null"); if (logger.isDebugEnabled()) { String sql = getSql(csc); - logger.debug("Calling stored procedure" + (sql != null ? " [" + sql + "]" : "")); + logger.debug("Calling stored procedure" + (sql != null ? " [" + sql + "]" : "")); } Connection con = DataSourceUtils.getConnection(obtainDataSource()); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java index 0b301f0411e2..5a602a649bab 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -87,8 +87,8 @@ void errorCodeTranslation() { SQLException dupKeyEx = new SQLException("", "", 10); DataAccessException dataAccessException = translator.translate("task", "SQL", dupKeyEx); assertThat(dataAccessException) - .isInstanceOf(DataIntegrityViolationException.class) - .hasCause(dupKeyEx); + .isInstanceOf(DataIntegrityViolationException.class) + .hasCause(dupKeyEx); // Test fallback. We assume that no database will ever return this error code, // but 07xxx will be bad grammar picked up by the fallback SQLState translator @@ -102,8 +102,8 @@ private void checkTranslation(int errorCode, Class expected SQLException sqlException = new SQLException("", "", errorCode); DataAccessException dataAccessException = this.translator.translate("", "", sqlException); assertThat(dataAccessException) - .isInstanceOf(expectedType) - .hasCause(sqlException); + .isInstanceOf(expectedType) + .hasCause(sqlException); } @Test @@ -122,8 +122,8 @@ void dataTruncationTranslation() { DataTruncation dataTruncation = new DataTruncation(1, true, true, 1, 1, dataAccessEx); DataAccessException dataAccessException = translator.translate("task", "SQL", dataTruncation); assertThat(dataAccessException) - .isInstanceOf(DataAccessResourceFailureException.class) - .hasCause(dataTruncation); + .isInstanceOf(DataAccessResourceFailureException.class) + .hasCause(dataTruncation); } @Test @@ -153,8 +153,8 @@ protected DataAccessException customTranslate(String task, @Nullable String sql, // Shouldn't custom translate this DataAccessException dataAccessException = translator.translate(TASK, SQL, integrityViolationEx); assertThat(dataAccessException) - .isInstanceOf(DataIntegrityViolationException.class) - .hasCause(integrityViolationEx); + .isInstanceOf(DataIntegrityViolationException.class) + .hasCause(integrityViolationEx); } @Test @@ -176,15 +176,15 @@ void customExceptionTranslation() { SQLException badSqlEx = new SQLException("", "", 1); DataAccessException dataAccessException = translator.translate(TASK, SQL, badSqlEx); assertThat(dataAccessException) - .isInstanceOf(CustomErrorCodeException.class) - .hasCause(badSqlEx); + .isInstanceOf(CustomErrorCodeException.class) + .hasCause(badSqlEx); // Shouldn't custom translate this SQLException invResEx = new SQLException("", "", 3); dataAccessException = translator.translate(TASK, SQL, invResEx); assertThat(dataAccessException) - .isInstanceOf(DataIntegrityViolationException.class) - .hasCause(invResEx); + .isInstanceOf(DataIntegrityViolationException.class) + .hasCause(invResEx); // Shouldn't custom translate this - invalid class assertThatIllegalArgumentException().isThrownBy(() -> customTranslation.setExceptionClass(String.class)); @@ -209,7 +209,8 @@ void dataSourceInitialization() throws Exception { reset(dataSource); given(dataSource.getConnection()).willReturn(connection); - assertThat(translator.translate("test", null, duplicateKeyException)).isInstanceOf(DuplicateKeyException.class); + assertThat(translator.translate("test", null, duplicateKeyException)) + .isInstanceOf(DuplicateKeyException.class); verify(connection).close(); } From 23eff5c650e8e8cc9d7c23a7282d255782a4998d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 9 Jan 2024 14:12:09 +0100 Subject: [PATCH 059/261] Update ContentRequestMatchers#multipartData Javadoc This commit updates ContentRequestMatchers#multipartData Javadoc to mention Tomcat fork of Commons FileUpload library instead of the original variant. It also adds a similar note to ContentRequestMatchers#multipartDataContains. Closes gh-31989 (Backport of 598c972a78346dfeff459a5ffa510e90f4907ee2) --- .../test/web/client/match/ContentRequestMatchers.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index 41e8f195e2a8..d99d98f28f63 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -197,8 +197,9 @@ private RequestMatcher formData(MultiValueMap expectedMap, boole *
  • {@link Resource} - content from a file *
  • {@code byte[]} - other raw content * - *

    Note: This method uses the Apache Commons FileUpload - * library to parse the multipart data and it must be on the test classpath. + *

    Note: This method uses the fork of Commons FileUpload library + * packaged with Apache Tomcat in the {@code org.apache.tomcat.util.http.fileupload} + * package to parse the multipart data and it must be on the test classpath. * @param expectedMap the expected multipart values * @since 5.3 */ @@ -209,6 +210,9 @@ public RequestMatcher multipartData(MultiValueMap expectedMap) { /** * Variant of {@link #multipartData(MultiValueMap)} that does the same but * only for a subset of the actual values. + *

    Note: This method uses the fork of Commons FileUpload library + * packaged with Apache Tomcat in the {@code org.apache.tomcat.util.http.fileupload} + * package to parse the multipart data and it must be on the test classpath. * @param expectedMap the expected multipart values * @since 5.3 */ From a1463c2bf2a6bf43e077d200b556136e2d5c48ec Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 9 Jan 2024 12:09:54 +0000 Subject: [PATCH 060/261] Do not set exception attribute if response body is set ResponseEntityExceptionHandler should not set the exception attribute when there is a response body, and the response is fully handled. Closes gh-31541 --- .../ResponseEntityExceptionHandler.java | 10 +++++----- .../ResponseEntityExceptionHandlerTests.java | 15 +++++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java index eb47bb6e0481..b2638327638f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -560,14 +560,14 @@ protected ResponseEntity handleExceptionInternal( } } - if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR)) { - request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); - } - if (body == null && ex instanceof ErrorResponse errorResponse) { body = errorResponse.updateAndGetBody(this.messageSource, LocaleContextHolder.getLocale()); } + if (statusCode.equals(HttpStatus.INTERNAL_SERVER_ERROR) && body == null) { + request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); + } + return createResponseEntity(body, headers, statusCode, request); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index 12529ef528ad..d30867ccab72 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -32,7 +32,6 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.http.ProblemDetail; @@ -267,6 +266,15 @@ public void asyncRequestTimeoutException() { testException(new AsyncRequestTimeoutException()); } + @Test // gh-14287, gh-31541 + void serverErrorWithoutBody() { + HttpStatusCode code = HttpStatusCode.valueOf(500); + Exception ex = new IllegalStateException("internal error"); + this.exceptionHandler.handleExceptionInternal(ex, null, new HttpHeaders(), code, this.request); + + assertThat(this.servletRequest.getAttribute("jakarta.servlet.error.exception")).isSameAs(ex); + } + @Test public void controllerAdvice() throws Exception { StaticWebApplicationContext ctx = new StaticWebApplicationContext(); @@ -343,11 +351,6 @@ private ResponseEntity testException(Exception ex) { try { ResponseEntity entity = this.exceptionHandler.handleException(ex, this.request); - // SPR-9653 - if (HttpStatus.INTERNAL_SERVER_ERROR.equals(entity.getStatusCode())) { - assertThat(this.servletRequest.getAttribute("jakarta.servlet.error.exception")).isSameAs(ex); - } - // Verify DefaultHandlerExceptionResolver would set the same status this.exceptionResolver.resolveException(this.servletRequest, this.servletResponse, null, ex); assertThat(entity.getStatusCode().value()).isEqualTo(this.servletResponse.getStatus()); From 4a599d0b222a759b3ddad546dc1773d17c9a33df Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 10 Jan 2024 11:09:11 +0100 Subject: [PATCH 061/261] Upgrade to Reactor 2022.0.15 Includes SLF4J 2.0.11, Groovy 4.0.17, Jetty 11.0.19, Netty 4.1.104, Apache HttpClient 5.2.3, POI 5.2.5, OpenPDF 1.3.36, Checkstyle 10.12.7 Closes gh-31997 --- build.gradle | 2 +- framework-platform/framework-platform.gradle | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index 06c34272d397..838d0f3a7896 100644 --- a/build.gradle +++ b/build.gradle @@ -75,7 +75,7 @@ configure([rootProject] + javaProjects) { project -> } checkstyle { - toolVersion = "10.12.5" + toolVersion = "10.12.7" configDirectory.set(rootProject.file("src/checkstyle")) } diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 7ae800491c56..3315655d6dbd 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,13 +9,13 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.14.3")) api(platform("io.micrometer:micrometer-bom:1.10.13")) - api(platform("io.netty:netty-bom:4.1.101.Final")) + api(platform("io.netty:netty-bom:4.1.104.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.14")) + api(platform("io.projectreactor:reactor-bom:2022.0.15")) api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.16")) + api(platform("org.apache.groovy:groovy-bom:4.0.17")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) - api(platform("org.eclipse.jetty:jetty-bom:11.0.18")) + api(platform("org.eclipse.jetty:jetty-bom:11.0.19")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) api(platform("org.junit:junit-bom:5.9.3")) @@ -25,7 +25,7 @@ dependencies { api("com.fasterxml:aalto-xml:1.3.2") api("com.fasterxml.woodstox:woodstox-core:6.5.1") api("com.github.ben-manes.caffeine:caffeine:3.1.8") - api("com.github.librepdf:openpdf:1.3.33") + api("com.github.librepdf:openpdf:1.3.36") api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:jsr305:3.0.2") api("com.google.code.gson:gson:2.10.1") @@ -96,9 +96,9 @@ dependencies { api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1") - api("org.apache.httpcomponents.client5:httpclient5:5.2.1") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.3") - api("org.apache.poi:poi-ooxml:5.2.4") + api("org.apache.httpcomponents.client5:httpclient5:5.2.3") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.2.4") + api("org.apache.poi:poi-ooxml:5.2.5") api("org.apache.tomcat.embed:tomcat-embed-core:10.1.15") api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.15") api("org.apache.tomcat:tomcat-util:10.1.15") @@ -135,7 +135,7 @@ dependencies { api("org.seleniumhq.selenium:htmlunit-driver:2.70.0") api("org.seleniumhq.selenium:selenium-java:3.141.59") api("org.skyscreamer:jsonassert:1.5.1") - api("org.slf4j:slf4j-api:2.0.9") + api("org.slf4j:slf4j-api:2.0.11") api("org.testng:testng:7.8.0") api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-core:0.55") From 6df8be8be3802f4e481aa284933ba3335f7f56c0 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 10 Jan 2024 12:33:28 +0000 Subject: [PATCH 062/261] Exclude query from URI in WebClient checkpoints Closes gh-31992 --- .../client/DefaultClientResponseBuilder.java | 4 ++-- .../function/client/DefaultWebClient.java | 23 +++++-------------- .../client/WebClientResponseException.java | 6 ++--- .../function/client/WebClientUtils.java | 22 +++++++++++++++++- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java index 5ba831ebd8ae..e089b1f3ccb2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -212,7 +212,7 @@ public ClientResponse build() { return new DefaultClientResponse(httpResponse, this.strategies, this.originalResponse != null ? this.originalResponse.logPrefix() : "", - this.request.getMethod() + " " + this.request.getURI(), + WebClientUtils.getRequestDescription(this.request.getMethod(), this.request.getURI()), () -> this.request); } 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 91a5b57b5774..31deb88261da 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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.web.reactive.function.client; import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.Charset; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -54,7 +53,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; import org.springframework.web.reactive.function.BodyExtractor; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; @@ -457,7 +455,9 @@ public Mono exchange() { observationContext.setRequest(request); Mono responseMono = filterFunction.apply(exchangeFunction) .exchange(request) - .checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]") + .checkpoint("Request to " + + WebClientUtils.getRequestDescription(request.method(), request.url()) + + " [DefaultWebClient]") .switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR); if (this.contextModifier != null) { responseMono = responseMono.contextWrite(this.contextModifier); @@ -693,24 +693,13 @@ private Mono applyStatusHandlers(ClientResponse response) { } Mono result = exMono.flatMap(Mono::error); return result.checkpoint(statusCode + " from " + - this.httpMethod + " " + getUriToLog(this.uri) + " [DefaultWebClient]"); + WebClientUtils.getRequestDescription(this.httpMethod, this.uri) + + " [DefaultWebClient]"); } } return null; } - private static URI getUriToLog(URI uri) { - if (StringUtils.hasText(uri.getQuery())) { - try { - uri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null); - } - catch (URISyntaxException ex) { - // ignore - } - } - return uri; - } - private static class StatusHandler { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 057cc0b09562..47e04f9afe8c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -97,8 +97,8 @@ public WebClientResponseException( } private static String initMessage(HttpStatusCode status, String reasonPhrase, @Nullable HttpRequest request) { - return status.value() + " " + reasonPhrase + - (request != null ? " from " + request.getMethod() + " " + request.getURI() : ""); + return status.value() + " " + reasonPhrase + (request != null ? + " from " + WebClientUtils.getRequestDescription(request.getMethod(), request.getURI()) : ""); } /** 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 dae54ccb1382..62cfd503bda9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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,8 @@ package org.springframework.web.reactive.function.client; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; import java.util.function.Predicate; @@ -24,7 +26,9 @@ import reactor.core.publisher.Mono; import org.springframework.core.codec.CodecException; +import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; /** * Internal methods shared between {@link DefaultWebClient} and @@ -64,4 +68,20 @@ public static Mono>> mapToEntityList(ClientResponse r new ResponseEntity<>(list, response.headers().asHttpHeaders(), response.statusCode())); } + /** + * Return a String representation of the request details for logging purposes. + * @since 6.0.16 + */ + public static String getRequestDescription(HttpMethod httpMethod, URI uri) { + if (StringUtils.hasText(uri.getQuery())) { + try { + uri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null); + } + catch (URISyntaxException ex) { + // ignore + } + } + return httpMethod.name() + " " + uri; + } + } From b8395a2321dca1a278c408c13f633313b85358eb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 10 Jan 2024 17:10:23 +0100 Subject: [PATCH 063/261] Upgrade to spring-javaformat-checkstyle 0.0.41 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 838d0f3a7896..3eec601cc83e 100644 --- a/build.gradle +++ b/build.gradle @@ -106,7 +106,7 @@ configure([rootProject] + javaProjects) { project -> // JSR-305 only used for non-required meta-annotations compileOnly("com.google.code.findbugs:jsr305") testCompileOnly("com.google.code.findbugs:jsr305") - checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.39") + checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.41") } ext.javadocLinks = [ From fad857e06a63bcde9aa7c5c4060c0fc30437e722 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 11 Jan 2024 10:33:22 +0000 Subject: [PATCH 064/261] Next development version (v6.0.17-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1f188bcc477b..0d3033eb9645 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.16-SNAPSHOT +version=6.0.17-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 338922f03dc91042faa50ea7b9f0443f38e0ace5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 12 Jan 2024 11:37:42 +0100 Subject: [PATCH 065/261] Find destroy methods in superclass interfaces Related tests will be added in https://github.com/spring-projects/spring-aot-smoke-tests. Closes gh-32017 --- .../beans/factory/support/DisposableBeanAdapter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index bd6e62fb463d..bb9e5b8d742d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -261,7 +261,7 @@ private Method determineDestroyMethod(String destroyMethodName) { if (destroyMethod != null) { return destroyMethod; } - for (Class beanInterface : beanClass.getInterfaces()) { + for (Class beanInterface : ClassUtils.getAllInterfacesForClass(beanClass)) { destroyMethod = findDestroyMethod(beanInterface, methodName); if (destroyMethod != null) { return destroyMethod; From 3f9d479583e16314ccf9e58d56f722de7ca6c056 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 12 Jan 2024 17:15:06 +0000 Subject: [PATCH 066/261] Double-checked lock in ChannelSendOperator#request Closes gh-31865 --- .../handler/invocation/reactive/ChannelSendOperator.java | 4 ++++ .../http/server/reactive/ChannelSendOperator.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java index 0fa659141b4f..f0b3ea29619a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java @@ -281,6 +281,10 @@ public void request(long n) { return; } synchronized (this) { + if (this.state == State.READY_TO_WRITE) { + s.request(n); + return; + } if (this.writeSubscriber != null) { if (this.state == State.EMITTING_CACHED_SIGNALS) { this.demandBeforeReadyToWrite = n; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java index 4cba68c3bfb8..63c4033a78d7 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java @@ -273,6 +273,10 @@ public void request(long n) { return; } synchronized (this) { + if (this.state == State.READY_TO_WRITE) { + s.request(n); + return; + } if (this.writeSubscriber != null) { if (this.state == State.EMITTING_CACHED_SIGNALS) { this.demandBeforeReadyToWrite = n; From 94bdd4380f8eb7c078067903e155cc051995d661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 16 Jan 2024 10:59:43 +0100 Subject: [PATCH 067/261] Fix incorrect assertions using json path Closes gh-32040 --- .../client/standalone/ExceptionHandlerTests.java | 12 ++++++------ .../client/standalone/RequestParameterTests.java | 2 +- .../client/standalone/ViewResolutionTests.java | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ExceptionHandlerTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ExceptionHandlerTests.java index 06b30d0ab24d..5e2f3b1c86bc 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ExceptionHandlerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ExceptionHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -110,7 +110,7 @@ void noException() { .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() - .expectBody().jsonPath("$.name", "Yoda"); + .expectBody().jsonPath("$.name").isEqualTo("Yoda"); } @Test @@ -123,7 +123,7 @@ void localExceptionHandlerMethod() { .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() - .expectBody().jsonPath("$.error", "local - IllegalArgumentException"); + .expectBody().jsonPath("$.error").isEqualTo("local - IllegalArgumentException"); } @Test @@ -136,7 +136,7 @@ void globalExceptionHandlerMethod() { .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() - .expectBody().jsonPath("$.error", "global - IllegalArgumentException"); + .expectBody().jsonPath("$.error").isEqualTo("global - IllegalStateException"); } @Test @@ -149,7 +149,7 @@ void globalRestPersonControllerExceptionHandlerTakesPrecedenceOverGlobalExceptio .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() - .expectBody().jsonPath("$.error", "globalPersonController - IllegalStateException"); + .expectBody().jsonPath("$.error").isEqualTo("globalPersonController - IllegalStateException"); } @Test @@ -163,7 +163,7 @@ void noHandlerFound() { .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() - .expectBody().jsonPath("$.error", "global - NoHandlerFoundException"); + .expectBody().jsonPath("$.error").isEqualTo("global - NoHandlerFoundException"); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RequestParameterTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RequestParameterTests.java index bb9f0efd4085..cc6ed1425d42 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RequestParameterTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/RequestParameterTests.java @@ -45,7 +45,7 @@ public void queryParameter() { .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON) - .expectBody().jsonPath("$.name", "George"); + .expectBody().jsonPath("$.name").isEqualTo("George"); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ViewResolutionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ViewResolutionTests.java index d1d06aa97dcc..000c101385b0 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ViewResolutionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ViewResolutionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -85,7 +85,7 @@ void jsonOnly() { .exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON) - .expectBody().jsonPath("$.person.name", "Corea"); + .expectBody().jsonPath("$.person.name").isEqualTo("Corea"); } @Test @@ -143,7 +143,7 @@ void contentNegotiation() throws Exception { .exchange() .expectStatus().isOk() .expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON) - .expectBody().jsonPath("$.person.name", "Corea"); + .expectBody().jsonPath("$.person.name").isEqualTo("Corea"); testClient.get().uri("/person/Corea") .accept(MediaType.APPLICATION_XML) From 328e444db3f1381003426227dbea7ed82031b7b3 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 17 Jan 2024 21:17:05 +0100 Subject: [PATCH 068/261] Upgrade CI image to Ubuntu Jammy 20240111 --- ci/images/ci-image/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index d541b6d92d6f..30dd69f0e431 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:jammy-20231211.1 +FROM ubuntu:jammy-20240111 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh From 3be4322f71a558f4afdea8160dd219d1d828ed41 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 17 Jan 2024 21:17:46 +0100 Subject: [PATCH 069/261] Upgrade CI image to JDK 17.0.10+13 --- ci/images/get-jdk-url.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index ab19da60336b..80c68d3dab7f 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -3,10 +3,7 @@ set -e case "$1" in java17) - echo "https://github.com/bell-sw/Liberica/releases/download/17.0.9+11/bellsoft-jdk17.0.9+11-linux-amd64.tar.gz" - ;; - java20) - echo "https://github.com/bell-sw/Liberica/releases/download/20.0.1+10/bellsoft-jdk20.0.1+10-linux-amd64.tar.gz" + echo "https://github.com/bell-sw/Liberica/releases/download/17.0.10%2B13/bellsoft-jdk17.0.10+13-linux-amd64.tar.gz" ;; *) echo $"Unknown java version" From d756c2b1286071a97940297733b930c60508d2ad Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 17 Jan 2024 21:25:16 +0100 Subject: [PATCH 070/261] Remove JDK 20 variant from CI build. --- ci/images/ci-image/Dockerfile | 1 - ci/images/setup.sh | 2 +- ci/pipeline.yml | 45 +---------------------------------- ci/scripts/check-project.sh | 2 +- 4 files changed, 3 insertions(+), 47 deletions(-) diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index 30dd69f0e431..bd35ac10e754 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -6,6 +6,5 @@ RUN ./setup.sh ENV JAVA_HOME /opt/openjdk/java17 ENV JDK17 /opt/openjdk/java17 -ENV JDK20 /opt/openjdk/java20 ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/setup.sh b/ci/images/setup.sh index 6724cd5619dc..ebdc7f843800 100755 --- a/ci/images/setup.sh +++ b/ci/images/setup.sh @@ -20,7 +20,7 @@ curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/c mkdir -p /opt/openjdk pushd /opt/openjdk > /dev/null -for jdk in java17 java20 +for jdk in java17 do JDK_URL=$( /get-jdk-url.sh $jdk ) mkdir $jdk diff --git a/ci/pipeline.yml b/ci/pipeline.yml index a35b12af4ae7..ac164eba4503 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -90,13 +90,6 @@ resources: <<: *docker-resource-source repository: ((docker-hub-organization))/spring-framework-ci tag: ((milestone)) -- name: every-morning - type: time - icon: alarm - source: - start: 8:00 AM - stop: 9:00 AM - location: Europe/Vienna - name: artifactory-repo type: artifactory-resource icon: package-variant @@ -113,14 +106,6 @@ resources: access_token: ((github-ci-status-token)) branch: ((branch)) context: build -- name: repo-status-jdk20-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: jdk20-build - name: slack-alert type: slack-notification icon: slack @@ -217,34 +202,6 @@ jobs: "zip.type": "schema" get_params: threads: 8 -- name: jdk20-build - serial: true - public: true - plan: - - get: ci-image - - get: git-repo - - get: every-morning - trigger: true - - put: repo-status-jdk20-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: check-project - image: ci-image - file: git-repo/ci/tasks/check-project.yml - privileged: true - timeout: ((task-timeout)) - params: - TEST_TOOLCHAIN: 20 - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-jdk20-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-jdk20-build - params: { state: "success", commit: "git-repo" } - name: stage-milestone serial: true plan: @@ -396,7 +353,7 @@ jobs: groups: - name: "builds" - jobs: ["build", "jdk20-build"] + jobs: ["build"] - name: "releases" jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] - name: "ci-images" diff --git a/ci/scripts/check-project.sh b/ci/scripts/check-project.sh index 4eb582e3689e..a55cb51a5f43 100755 --- a/ci/scripts/check-project.sh +++ b/ci/scripts/check-project.sh @@ -4,6 +4,6 @@ set -e source $(dirname $0)/common.sh pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK20 \ +./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17 \ -PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check antora popd > /dev/null From 38595c6a990e0190fd824c958c1ae89f9a853d57 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 18 Jan 2024 15:32:01 +0100 Subject: [PATCH 071/261] Handle Content-Length in ShallowEtagHeaderFilter more robustly This commit ensures that setting the Content-Length through setHeader("Content-Length", x") has the same effect as calling setContentLength in the ShallowEtagHeaderFilter. It also filters out Content-Type headers similarly to Content-Length. See gh-32039 Closes gh-32050 --- .../util/ContentCachingResponseWrapper.java | 139 +++++++++++++++++- .../filter/ShallowEtagHeaderFilterTests.java | 9 +- 2 files changed, 142 insertions(+), 6 deletions(-) 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 dd9a46cab6fc..4223a673976c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,10 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.WriteListener; @@ -55,6 +59,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper { @Nullable private Integer contentLength; + @Nullable + private String contentType; + /** * Create a new ContentCachingResponseWrapper for the given servlet response. @@ -139,6 +146,122 @@ public void setContentLengthLong(long len) { this.contentLength = lenInt; } + @Override + public void setContentType(String type) { + this.contentType = type; + } + + @Override + @Nullable + public String getContentType() { + return this.contentType; + } + + @Override + public boolean containsHeader(String name) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return this.contentLength != null; + } + else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + return this.contentType != null; + } + else { + return super.containsHeader(name); + } + } + + @Override + public void setHeader(String name, String value) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = Integer.valueOf(value); + } + else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + this.contentType = value; + } + else { + super.setHeader(name, value); + } + } + + @Override + public void addHeader(String name, String value) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = Integer.valueOf(value); + } + else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + this.contentType = value; + } + else { + super.addHeader(name, value); + } + } + + @Override + public void setIntHeader(String name, int value) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = Integer.valueOf(value); + } + else { + super.setIntHeader(name, value); + } + } + + @Override + public void addIntHeader(String name, int value) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = Integer.valueOf(value); + } + else { + super.addIntHeader(name, value); + } + } + + @Override + @Nullable + public String getHeader(String name) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return (this.contentLength != null) ? this.contentLength.toString() : null; + } + else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + return this.contentType; + } + else { + return super.getHeader(name); + } + } + + @Override + public Collection getHeaders(String name) { + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return this.contentLength != null ? Collections.singleton(this.contentLength.toString()) : + Collections.emptySet(); + } + else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + return this.contentType != null ? Collections.singleton(this.contentType) : Collections.emptySet(); + } + else { + return super.getHeaders(name); + } + } + + @Override + public Collection getHeaderNames() { + Collection headerNames = super.getHeaderNames(); + if (this.contentLength != null || this.contentType != null) { + List result = new ArrayList<>(headerNames); + if (this.contentLength != null) { + result.add(HttpHeaders.CONTENT_LENGTH); + } + if (this.contentType != null) { + result.add(HttpHeaders.CONTENT_TYPE); + } + return result; + } + else { + return headerNames; + } + } + @Override public void setBufferSize(int size) { if (size > this.content.size()) { @@ -197,11 +320,17 @@ public void copyBodyToResponse() throws IOException { protected void copyBodyToResponse(boolean complete) throws IOException { if (this.content.size() > 0) { HttpServletResponse rawResponse = (HttpServletResponse) getResponse(); - if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) { - if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) { - rawResponse.setContentLength(complete ? this.content.size() : this.contentLength); + if (!rawResponse.isCommitted()) { + if (complete || this.contentLength != null) { + if (rawResponse.getHeader(HttpHeaders.TRANSFER_ENCODING) == null) { + rawResponse.setContentLength(complete ? this.content.size() : this.contentLength); + } + this.contentLength = null; + } + if (complete || this.contentType != null) { + rawResponse.setContentType(this.contentType); + this.contentType = null; } - this.contentLength = null; } this.content.writeTo(rawResponse.getOutputStream()); this.content.reset(); diff --git a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java index 8ab3db1165c6..3156b8b34760 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java @@ -23,6 +23,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; @@ -68,6 +69,7 @@ public void filterNoMatch() throws Exception { FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); @@ -75,6 +77,7 @@ public void filterNoMatch() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(200); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody); } @@ -88,6 +91,7 @@ public void filterNoMatchWeakETag() throws Exception { FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); @@ -95,6 +99,7 @@ public void filterNoMatchWeakETag() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(200); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("W/\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody); } @@ -108,14 +113,16 @@ public void filterMatch() throws Exception { FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); - FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); filterResponse.setContentLength(responseBody.length); + filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE); + FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); assertThat(response.getStatus()).as("Invalid status").isEqualTo(304); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse(); + assertThat(response.containsHeader("Content-Type")).as("Response has Content-Type header").isFalse(); byte[] expecteds = new byte[0]; assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds); } From c6684731865e5699a28f30353157ef1dcbe02b17 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 19 Jan 2024 17:18:28 +0100 Subject: [PATCH 072/261] Polishing --- .../org/springframework/context/SmartLifecycle.java | 4 ++-- .../PersistenceManagedTypesScanner.java | 7 ++++++- .../transaction/reactive/TransactionContext.java | 11 +++++++---- .../http/server/reactive/ChannelSendOperator.java | 4 ++-- .../servlet/handler/AbstractHandlerMethodMapping.java | 3 ++- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java index 06f98a4048ca..5ad500d1b211 100644 --- a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java +++ b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -121,7 +121,7 @@ default void stop(Runnable callback) { /** * Return the phase that this lifecycle object is supposed to run in. *

    The default implementation returns {@link #DEFAULT_PHASE} in order to - * let {@code stop()} callbacks execute after regular {@code Lifecycle} + * let {@code stop()} callbacks execute before regular {@code Lifecycle} * implementations. * @see #isAutoStartup() * @see #start() diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java index 48b5d11fa6d3..09649fe16118 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -73,11 +73,16 @@ public final class PersistenceManagedTypesScanner { private final CandidateComponentsIndex componentsIndex; + /** + * Create a new {@code PersistenceManagedTypesScanner} for the given resource loader. + * @param resourceLoader the {@code ResourceLoader} to use + */ public PersistenceManagedTypesScanner(ResourceLoader resourceLoader) { this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader); this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(resourceLoader.getClassLoader()); } + /** * Scan the specified packages and return a {@link PersistenceManagedTypes} that * represents the result of the scanning. diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContext.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContext.java index 3fe4ad088d4b..9a6c191a916c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContext.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,18 +36,21 @@ */ public class TransactionContext { - private final @Nullable TransactionContext parent; + @Nullable + private final TransactionContext parent; private final Map resources = new LinkedHashMap<>(); @Nullable private Set synchronizations; - private volatile @Nullable String currentTransactionName; + @Nullable + private volatile String currentTransactionName; private volatile boolean currentTransactionReadOnly; - private volatile @Nullable Integer currentTransactionIsolationLevel; + @Nullable + private volatile Integer currentTransactionIsolationLevel; private volatile boolean actualTransactionActive; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java index 63c4033a78d7..abf0b6ab8fcd 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ChannelSendOperator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -39,7 +39,7 @@ * to defer the invocation of the write function, until we know if the source * publisher will begin publishing without an error. If the first emission is * an error, the write function is bypassed, and the error is sent directly - * through the result publisher. Otherwise the write function is invoked. + * through the result publisher. Otherwise, the write function is invoked. * * @author Rossen Stoyanchev * @author Stephane Maldini diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index f9f5c68e429c..554e0fe8dcfa 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -565,6 +565,7 @@ protected Set getDirectPaths(T mapping) { /** * A registry that maintains all mappings to handler methods, exposing methods * to perform lookups and providing concurrent access. + * *

    Package-private for testing purposes. */ class MappingRegistry { From b484ab116f818129471d3bf19feea2a9df1f2c92 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 22 Jan 2024 11:03:57 +0100 Subject: [PATCH 073/261] Record errors thrown by custom handler in RestTemplate observations Prior to this commit, the `RestTemplate` observation instrumentation would only record `RestClientException` and `IOException` as errors in the observation. Other types of errors can be thrown by custom components, such as `ResponseErrorHandler` and in this case they aren't recorded with the observation. Also, the current instrumentation does not create any observation scope around the execution. While this would have a limited benefit as no application code is executed there, developers could set up custom components (such as, again, `ResponseErrorHandler`) that could use contextual logging with trace ids. This commit ensures that all `Throwable` are recorded as errors with the observations and that an observation `Scope` is created around the execution of the client exchange. Fixes gh-32063 --- .../web/client/RestTemplate.java | 4 +- .../client/RestTemplateObservationTests.java | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 19e42a303979..f88c41b7c163 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -855,7 +855,7 @@ protected T doExecute(URI url, @Nullable String uriTemplate, @Nullable HttpM Observation observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).start(); ClientHttpResponse response = null; - try { + try (Observation.Scope scope = observation.openScope()){ if (requestCallback != null) { requestCallback.doWithRequest(request); } @@ -869,7 +869,7 @@ protected T doExecute(URI url, @Nullable String uriTemplate, @Nullable HttpM observation.error(exception); throw exception; } - catch (RestClientException exc) { + catch (Throwable exc) { observation.error(exc); throw exc; } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateObservationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateObservationTests.java index 97eb7fc5fef9..2080b265ad0e 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateObservationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateObservationTests.java @@ -39,9 +39,11 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.observation.ClientRequestObservationContext; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -158,6 +160,31 @@ void executeWithIoExceptionAddsUnknownOutcome() throws Exception { assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN"); } + @Test // gh-32060 + void executeShouldRecordErrorsThrownByErrorHandler() throws Exception { + mockSentRequest(GET, "https://example.org"); + mockResponseStatus(HttpStatus.OK); + mockResponseBody("Hello World", MediaType.TEXT_PLAIN); + given(errorHandler.hasError(any())).willThrow(new IllegalStateException("error handler")); + + assertThatIllegalStateException().isThrownBy(() -> + template.execute("https://example.org", GET, null, null)); + + assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "IllegalStateException"); + } + + @Test // gh-32060 + void executeShouldCreateObservationScope() throws Exception { + mockSentRequest(GET, "https://example.org"); + mockResponseStatus(HttpStatus.OK); + mockResponseBody("Hello World", MediaType.TEXT_PLAIN); + ObservationErrorHandler observationErrorHandler = new ObservationErrorHandler(observationRegistry); + template.setErrorHandler(observationErrorHandler); + + template.execute("https://example.org", GET, null, null); + assertThat(observationErrorHandler.currentObservation).isNotNull(); + } + private void mockSentRequest(HttpMethod method, String uri) throws Exception { mockSentRequest(method, uri, new HttpHeaders()); @@ -205,4 +232,26 @@ public void onStart(ClientRequestObservationContext context) { } } + static class ObservationErrorHandler implements ResponseErrorHandler { + + final TestObservationRegistry observationRegistry; + + @Nullable + Observation currentObservation; + + public ObservationErrorHandler(TestObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + @Override + public boolean hasError(ClientHttpResponse response) throws IOException { + return true; + } + + @Override + public void handleError(ClientHttpResponse response) throws IOException { + currentObservation = this.observationRegistry.getCurrentObservation(); + } + } + } From e2a5cfb4591c2460d7d1ff26047a3ced0e6e2020 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 24 Jan 2024 11:43:36 +0100 Subject: [PATCH 074/261] Consistent nullability for concurrent result (cherry picked from commit b92877990da57c36eee035095c9ae72f5c0a7537) --- .../web/context/request/async/WebAsyncManager.java | 3 ++- .../method/annotation/RequestMappingHandlerAdapter.java | 7 +++++-- .../method/annotation/ServletInvocableHandlerMethod.java | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 6f7be997a24d..220996ee7056 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -159,6 +159,7 @@ public boolean hasConcurrentResult() { * concurrent handling raised one. * @see #clearConcurrentResult() */ + @Nullable public Object getConcurrentResult() { return this.concurrentResult; } 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 b9429cc841db..28dbf3243ba5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.ui.ModelMap; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.web.accept.ContentNegotiationManager; @@ -872,7 +873,9 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); - mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; + Object[] resultContext = asyncManager.getConcurrentResultContext(); + Assert.state(resultContext != null && resultContext.length > 0, "Missing result context"); + mavContainer = (ModelAndViewContainer) resultContext[0]; asyncManager.clearConcurrentResult(); LogFormatUtils.traceDebug(logger, traceOn -> { String formatted = LogFormatUtils.formatValue(result, !traceOn); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index a7b93891f87e..0b98a52f5731 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -200,7 +200,7 @@ private String formatErrorForReturnValue(@Nullable Object returnValue) { * actually invoking the controller method. This is useful when processing * async return values (e.g. Callable, DeferredResult, ListenableFuture). */ - ServletInvocableHandlerMethod wrapConcurrentResult(Object result) { + ServletInvocableHandlerMethod wrapConcurrentResult(@Nullable Object result) { return new ConcurrentResultHandlerMethod(result, new ConcurrentResultMethodParameter(result)); } @@ -215,7 +215,7 @@ private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMetho private final MethodParameter returnType; - public ConcurrentResultHandlerMethod(final Object result, ConcurrentResultMethodParameter returnType) { + public ConcurrentResultHandlerMethod(@Nullable Object result, ConcurrentResultMethodParameter returnType) { super((Callable) () -> { if (result instanceof Exception exception) { throw exception; @@ -279,7 +279,7 @@ private class ConcurrentResultMethodParameter extends HandlerMethodParameter { private final ResolvableType returnType; - public ConcurrentResultMethodParameter(Object returnValue) { + public ConcurrentResultMethodParameter(@Nullable Object returnValue) { super(-1); this.returnValue = returnValue; this.returnType = (returnValue instanceof CollectedValuesList cvList ? From c6e9cd0c622f7d0fb2ebd7b528ff832681473c87 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 24 Jan 2024 11:59:15 +0100 Subject: [PATCH 075/261] Polishing --- .../MockMvcClientHttpRequestFactory.java | 1 - .../test/web/servlet/MvcResult.java | 6 +-- .../web/client/RestOperations.java | 47 +++++++++---------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java b/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java index b64472c9b4ea..fc5b1bc70f81 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java @@ -35,7 +35,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; - /** * A {@link ClientHttpRequestFactory} for requests executed via {@link MockMvc}. * diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java index 6245fa0a6163..31b2e15b4721 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -34,13 +34,13 @@ public interface MvcResult { /** * Return the performed request. - * @return the request, never {@code null} + * @return the request (never {@code null}) */ MockHttpServletRequest getRequest(); /** * Return the resulting response. - * @return the response, never {@code null} + * @return the response (never {@code null}) */ MockHttpServletResponse getResponse(); diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java index 4ce6fc72638d..440fad582a5e 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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 @@ ResponseEntity getForEntity(String url, Class responseType, MapURI Template variables are expanded using the given URI variables, if any. *

    The {@code request} parameter can be a {@link HttpEntity} in order to @@ -165,7 +165,7 @@ ResponseEntity getForEntity(String url, Class responseType, MapURI Template variables are expanded using the given map. *

    The {@code request} parameter can be a {@link HttpEntity} in order to @@ -186,7 +186,7 @@ URI postForLocation(String url, @Nullable Object request, Map uriVari throws RestClientException; /** - * Create a new resource by POSTing the given object to the URL, and returns the value of the + * Create a new resource by POSTing the given object to the URL, and return the value of the * {@code Location} header. This header typically indicates where the new resource is stored. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -205,7 +205,7 @@ URI postForLocation(String url, @Nullable Object request, Map uriVari /** * Create a new resource by POSTing the given object to the URI template, - * and returns the representation found in the response. + * and return the representation found in the response. *

    URI Template variables are expanded using the given URI variables, if any. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -227,7 +227,7 @@ T postForObject(String url, @Nullable Object request, Class responseType, /** * Create a new resource by POSTing the given object to the URI template, - * and returns the representation found in the response. + * and return the representation found in the response. *

    URI Template variables are expanded using the given map. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -249,7 +249,7 @@ T postForObject(String url, @Nullable Object request, Class responseType, /** * Create a new resource by POSTing the given object to the URL, - * and returns the representation found in the response. + * and return the representation found in the response. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    The body of the entity, or {@code request} itself, can be a @@ -268,7 +268,7 @@ T postForObject(String url, @Nullable Object request, Class responseType, /** * Create a new resource by POSTing the given object to the URI template, - * and returns the response as {@link ResponseEntity}. + * and return the response as {@link ResponseEntity}. *

    URI Template variables are expanded using the given URI variables, if any. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -289,7 +289,7 @@ ResponseEntity postForEntity(String url, @Nullable Object request, Class< /** * Create a new resource by POSTing the given object to the URI template, - * and returns the response as {@link HttpEntity}. + * and return the response as {@link HttpEntity}. *

    URI Template variables are expanded using the given map. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. @@ -310,7 +310,7 @@ ResponseEntity postForEntity(String url, @Nullable Object request, Class< /** * Create a new resource by POSTing the given object to the URL, - * and returns the response as {@link ResponseEntity}. + * and return the response as {@link ResponseEntity}. *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    The body of the entity, or {@code request} itself, can be a @@ -374,7 +374,7 @@ ResponseEntity postForEntity(URI url, @Nullable Object request, Class *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    NOTE: The standard JDK HTTP library does not support HTTP PATCH. - * You need to use the Apache HttpComponents or OkHttp request factory. + * You need to use e.g. the Apache HttpComponents request factory. * @param url the URL * @param request the object to be PATCHed (may be {@code null}) * @param responseType the type of the return value @@ -384,7 +384,6 @@ ResponseEntity postForEntity(URI url, @Nullable Object request, Class * @see HttpEntity * @see RestTemplate#setRequestFactory * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory - * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory */ @Nullable T patchForObject(String url, @Nullable Object request, Class responseType, Object... uriVariables) @@ -397,7 +396,7 @@ T patchForObject(String url, @Nullable Object request, Class responseType *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    NOTE: The standard JDK HTTP library does not support HTTP PATCH. - * You need to use the Apache HttpComponents or OkHttp request factory. + * You need to use e.g. the Apache HttpComponents request factory. * @param url the URL * @param request the object to be PATCHed (may be {@code null}) * @param responseType the type of the return value @@ -407,7 +406,6 @@ T patchForObject(String url, @Nullable Object request, Class responseType * @see HttpEntity * @see RestTemplate#setRequestFactory * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory - * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory */ @Nullable T patchForObject(String url, @Nullable Object request, Class responseType, @@ -419,7 +417,7 @@ T patchForObject(String url, @Nullable Object request, Class responseType *

    The {@code request} parameter can be a {@link HttpEntity} in order to * add additional HTTP headers to the request. *

    NOTE: The standard JDK HTTP library does not support HTTP PATCH. - * You need to use the Apache HttpComponents or OkHttp request factory. + * You need to use e.g. the Apache HttpComponents request factory. * @param url the URL * @param request the object to be PATCHed (may be {@code null}) * @param responseType the type of the return value @@ -428,7 +426,6 @@ T patchForObject(String url, @Nullable Object request, Class responseType * @see HttpEntity * @see RestTemplate#setRequestFactory * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory - * @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory */ @Nullable T patchForObject(URI url, @Nullable Object request, Class responseType) @@ -492,8 +489,8 @@ T patchForObject(URI url, @Nullable Object request, Class responseType) // exchange /** - * Execute the HTTP method to the given URI template, writing the given request entity to the request, and - * returns the response as {@link ResponseEntity}. + * Execute the HTTP method to the given URI template, writing the given request entity to the request, + * and return the response as {@link ResponseEntity}. *

    URI Template variables are expanded using the given URI variables, if any. * @param url the URL * @param method the HTTP method (GET, POST, etc) @@ -508,8 +505,8 @@ ResponseEntity exchange(String url, HttpMethod method, @Nullable HttpEnti Class responseType, Object... uriVariables) throws RestClientException; /** - * Execute the HTTP method to the given URI template, writing the given request entity to the request, and - * returns the response as {@link ResponseEntity}. + * Execute the HTTP method to the given URI template, writing the given request entity to the request, + * and return the response as {@link ResponseEntity}. *

    URI Template variables are expanded using the given URI variables, if any. * @param url the URL * @param method the HTTP method (GET, POST, etc) @@ -524,8 +521,8 @@ ResponseEntity exchange(String url, HttpMethod method, @Nullable HttpEnti Class responseType, Map uriVariables) throws RestClientException; /** - * Execute the HTTP method to the given URI template, writing the given request entity to the request, and - * returns the response as {@link ResponseEntity}. + * Execute the HTTP method to the given URI template, writing the given request entity to the request, + * and return the response as {@link ResponseEntity}. * @param url the URL * @param method the HTTP method (GET, POST, etc) * @param requestEntity the entity (headers and/or body) to write to the request @@ -539,7 +536,7 @@ ResponseEntity exchange(URI url, HttpMethod method, @Nullable HttpEntity< /** * Execute the HTTP method to the given URI template, writing the given - * request entity to the request, and returns the response as {@link ResponseEntity}. + * request entity to the request, and return the response as {@link ResponseEntity}. * The given {@link ParameterizedTypeReference} is used to pass generic type information: *

     	 * ParameterizedTypeReference<List<MyBean>> myBean =
    @@ -562,7 +559,7 @@  ResponseEntity exchange(String url,HttpMethod method, @Nullable HttpEntit
     
     	/**
     	 * Execute the HTTP method to the given URI template, writing the given
    -	 * request entity to the request, and returns the response as {@link ResponseEntity}.
    +	 * request entity to the request, and return the response as {@link ResponseEntity}.
     	 * The given {@link ParameterizedTypeReference} is used to pass generic type information:
     	 * 
     	 * ParameterizedTypeReference<List<MyBean>> myBean =
    @@ -585,7 +582,7 @@  ResponseEntity exchange(String url, HttpMethod method, @Nullable HttpEnti
     
     	/**
     	 * Execute the HTTP method to the given URI template, writing the given
    -	 * request entity to the request, and returns the response as {@link ResponseEntity}.
    +	 * request entity to the request, and return the response as {@link ResponseEntity}.
     	 * The given {@link ParameterizedTypeReference} is used to pass generic type information:
     	 * 
     	 * ParameterizedTypeReference<List<MyBean>> myBean =
    
    From 9bd2be80b9200155e7e4fc7363288aa78910d5d0 Mon Sep 17 00:00:00 2001
    From: Juergen Hoeller 
    Date: Wed, 24 Jan 2024 12:22:51 +0100
    Subject: [PATCH 076/261] Declare allowPrivateNetwork as available since 5.3.32
    
    See gh-28546
    See gh-31974
    ---
     .../springframework/web/bind/annotation/CrossOrigin.java  | 4 ++--
     .../org/springframework/web/cors/CorsConfiguration.java   | 8 ++++----
     .../web/reactive/config/CorsRegistration.java             | 4 ++--
     .../web/servlet/config/annotation/CorsRegistration.java   | 4 ++--
     4 files changed, 10 insertions(+), 10 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 8f5a1e7f421b..7ee9ce421bc2 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
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2023 the original author or authors.
    + * Copyright 2002-2024 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.
    @@ -120,7 +120,7 @@
     	 * Whether private network access is supported. Please, see
     	 * {@link CorsConfiguration#setAllowPrivateNetwork(Boolean)} for details.
     	 * 

    By default this is not set (i.e. private network access is not supported). - * @since 6.1.3 + * @since 5.3.32 */ String allowPrivateNetwork() 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 26c9592b2db2..dbb22c425328 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -478,7 +478,7 @@ public Boolean getAllowCredentials() { * origins} and {@link #setAllowedOriginPatterns(List) originPatterns} are processed, * see related API documentation for more details. *

    By default this is not set (i.e. private network access is not supported). - * @since 6.1.3 + * @since 5.3.32 * @see Private network access specifications */ public void setAllowPrivateNetwork(@Nullable Boolean allowPrivateNetwork) { @@ -487,7 +487,7 @@ public void setAllowPrivateNetwork(@Nullable Boolean allowPrivateNetwork) { /** * Return the configured {@code allowPrivateNetwork} flag, or {@code null} if none. - * @since 6.1.3 + * @since 5.3.32 * @see #setAllowPrivateNetwork(Boolean) */ @Nullable @@ -582,7 +582,7 @@ public void validateAllowCredentials() { * {@link #setAllowedOrigins allowedOrigins} does not contain the special * value {@code "*"} since this is insecure. * @throws IllegalArgumentException if the validation fails - * @since 6.1.3 + * @since 5.3.32 */ public void validateAllowPrivateNetwork() { if (this.allowPrivateNetwork == Boolean.TRUE && 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 32c15d439914..bb7dccf290b1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -135,7 +135,7 @@ public CorsRegistration allowCredentials(boolean allowCredentials) { * Whether private network access is supported. *

    Please, see {@link CorsConfiguration#setAllowPrivateNetwork(Boolean)} for details. *

    By default this is not set (i.e. private network access is not supported). - * @since 6.1.3 + * @since 5.3.32 */ public CorsRegistration allowPrivateNetwork(boolean allowPrivateNetwork) { this.config.setAllowPrivateNetwork(allowPrivateNetwork); 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 cd37917349a2..55a8b0fc9bb8 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -135,7 +135,7 @@ public CorsRegistration allowCredentials(boolean allowCredentials) { /** * Whether private network access is supported. *

    By default this is not set (i.e. private network access is not supported). - * @since 6.1.3 + * @since 5.3.32 * @see Private network access specifications */ public CorsRegistration allowPrivateNetwork(boolean allowPrivateNetwork) { From 3817936ca5efda0c231f2b33ac9271ac834105a6 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 24 Jan 2024 12:35:30 +0100 Subject: [PATCH 077/261] Declare current observation context as available since 6.0.15 See gh-31609 See gh-31646 --- .../ClientRequestObservationContext.java | 24 +++++----- .../ClientRequestObservationContext.java | 45 ++++++++++--------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/observation/ClientRequestObservationContext.java b/spring-web/src/main/java/org/springframework/http/client/observation/ClientRequestObservationContext.java index 43a056e2a72b..a150dc8d9555 100644 --- a/spring-web/src/main/java/org/springframework/http/client/observation/ClientRequestObservationContext.java +++ b/spring-web/src/main/java/org/springframework/http/client/observation/ClientRequestObservationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -23,10 +23,12 @@ import org.springframework.lang.Nullable; /** - * Context that holds information for metadata collection - * during the {@link ClientHttpObservationDocumentation#HTTP_CLIENT_EXCHANGES client HTTP exchanges} observations. + * Context that holds information for metadata collection during the + * {@link ClientHttpObservationDocumentation#HTTP_CLIENT_EXCHANGES client HTTP exchanges} observations. + * *

    This context also extends {@link RequestReplySenderContext} for propagating tracing * information with the HTTP client exchange. + * * @author Brian Clozel * @since 6.0 */ @@ -35,6 +37,7 @@ public class ClientRequestObservationContext extends RequestReplySenderContextThe {@link #getCarrier() tracing context carrier} is a {@link ClientRequest.Builder request builder}, - * since the actual request is immutable. For {@code KeyValue} extraction, the {@link #getRequest() actual request} - * should be used instead. + * since the actual request is immutable. For {@code KeyValue} extraction, + * the {@link #getRequest() actual request} should be used instead. * * @author Brian Clozel * @since 6.0 @@ -37,10 +38,11 @@ public class ClientRequestObservationContext extends RequestReplySenderContext findCurrent(ClientRequest request) { return Optional.ofNullable((ClientRequestObservationContext) request.attributes().get(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE)); } + } From c749a14326c0644c59275b20078f0be3427919b5 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Wed, 24 Jan 2024 13:24:55 +0100 Subject: [PATCH 078/261] Guard against multiple body subscriptions Before this commit, the JDK and Jetty connectors do not have any safeguards against multiple body subscriptions. Such as check has now been added. See gh-32100 Closes gh-32102 --- .../reactive/AbstractClientHttpResponse.java | 90 +++++++++++++++++++ .../HttpComponentsClientHttpResponse.java | 55 +++--------- .../reactive/JdkClientHttpResponse.java | 50 ++++------- .../reactive/JettyClientHttpResponse.java | 55 ++++-------- .../client/WebClientIntegrationTests.java | 14 +-- 5 files changed, 140 insertions(+), 124 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java new file mode 100644 index 000000000000..4b128b047485 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2024 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.client.reactive; + +import java.util.concurrent.atomic.AtomicBoolean; + +import reactor.core.publisher.Flux; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseCookie; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +/** + * Base class for {@link ClientHttpResponse} implementations. + * + * @author Arjen Poutsma + * @since 5.3.32 + */ +public abstract class AbstractClientHttpResponse implements ClientHttpResponse { + + private final HttpStatusCode statusCode; + + private final HttpHeaders headers; + + private final MultiValueMap cookies; + + private final Flux body; + + + + protected AbstractClientHttpResponse(HttpStatusCode statusCode, HttpHeaders headers, + MultiValueMap cookies, Flux body) { + + Assert.notNull(statusCode, "StatusCode must not be null"); + Assert.notNull(headers, "Headers must not be null"); + Assert.notNull(body, "Body must not be null"); + + this.statusCode = statusCode; + this.headers = headers; + this.cookies = cookies; + this.body = singleSubscription(body); + } + + private static Flux singleSubscription(Flux body) { + AtomicBoolean subscribed = new AtomicBoolean(); + return body.doOnSubscribe(s -> { + if (!subscribed.compareAndSet(false, true)) { + throw new IllegalStateException("The client response body can only be consumed once"); + } + }); + } + + + @Override + public HttpStatusCode getStatusCode() { + return this.statusCode; + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public MultiValueMap getCookies() { + return this.cookies; + } + + @Override + public Flux getBody() { + return this.body; + } +} diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java index 45cd63bd7907..d64219e195a8 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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.client.reactive; import java.nio.ByteBuffer; -import java.util.concurrent.atomic.AtomicBoolean; import org.apache.hc.client5.http.cookie.Cookie; import org.apache.hc.client5.http.protocol.HttpClientContext; @@ -26,7 +25,6 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; @@ -42,40 +40,22 @@ * @since 5.3 * @see Apache HttpComponents */ -class HttpComponentsClientHttpResponse implements ClientHttpResponse { - - private final DataBufferFactory dataBufferFactory; - - private final Message> message; - - private final HttpHeaders headers; - - private final HttpClientContext context; - - private final AtomicBoolean rejectSubscribers = new AtomicBoolean(); +class HttpComponentsClientHttpResponse extends AbstractClientHttpResponse { public HttpComponentsClientHttpResponse(DataBufferFactory dataBufferFactory, Message> message, HttpClientContext context) { - this.dataBufferFactory = dataBufferFactory; - this.message = message; - this.context = context; - - MultiValueMap adapter = new HttpComponentsHeadersAdapter(message.getHead()); - this.headers = HttpHeaders.readOnlyHttpHeaders(adapter); + super(HttpStatusCode.valueOf(message.getHead().getCode()), + HttpHeaders.readOnlyHttpHeaders(new HttpComponentsHeadersAdapter(message.getHead())), + adaptCookies(context), + Flux.from(message.getBody()).map(dataBufferFactory::wrap) + ); } - - @Override - public HttpStatusCode getStatusCode() { - return HttpStatusCode.valueOf(this.message.getHead().getCode()); - } - - @Override - public MultiValueMap getCookies() { + private static MultiValueMap adaptCookies(HttpClientContext context) { LinkedMultiValueMap result = new LinkedMultiValueMap<>(); - this.context.getCookieStore().getCookies().forEach(cookie -> + context.getCookieStore().getCookies().forEach(cookie -> result.add(cookie.getName(), ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) .domain(cookie.getDomain()) @@ -88,25 +68,10 @@ public MultiValueMap getCookies() { return result; } - private long getMaxAgeSeconds(Cookie cookie) { + private static long getMaxAgeSeconds(Cookie cookie) { String maxAgeAttribute = cookie.getAttribute(Cookie.MAX_AGE_ATTR); return (maxAgeAttribute != null ? Long.parseLong(maxAgeAttribute) : -1); } - @Override - public Flux getBody() { - return Flux.from(this.message.getBody()) - .doOnSubscribe(s -> { - if (!this.rejectSubscribers.compareAndSet(false, true)) { - throw new IllegalStateException("The client response body can only be consumed once."); - } - }) - .map(this.dataBufferFactory::wrap); - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java index e0872ffd3c94..5574ac42fe1f 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -50,24 +50,20 @@ * @author Rossen Stoyanchev * @since 6.0 */ -class JdkClientHttpResponse implements ClientHttpResponse { +class JdkClientHttpResponse extends AbstractClientHttpResponse { private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); - private final HttpResponse>> response; - private final DataBufferFactory bufferFactory; + public JdkClientHttpResponse(HttpResponse>> response, + DataBufferFactory bufferFactory) { - private final HttpHeaders headers; - - - public JdkClientHttpResponse( - HttpResponse>> response, DataBufferFactory bufferFactory) { - - this.response = response; - this.bufferFactory = bufferFactory; - this.headers = adaptHeaders(response); + super(HttpStatusCode.valueOf(response.statusCode()), + adaptHeaders(response), + adaptCookies(response), + adaptBody(response, bufferFactory) + ); } private static HttpHeaders adaptHeaders(HttpResponse>> response) { @@ -78,20 +74,8 @@ private static HttpHeaders adaptHeaders(HttpResponse getCookies() { - return this.response.headers().allValues(HttpHeaders.SET_COOKIE).stream() + private static MultiValueMap adaptCookies(HttpResponse>> response) { + return response.headers().allValues(HttpHeaders.SET_COOKIE).stream() .flatMap(header -> { Matcher matcher = SAME_SITE_PATTERN.matcher(header); String sameSite = (matcher.matches() ? matcher.group(1) : null); @@ -102,7 +86,7 @@ public MultiValueMap getCookies() { LinkedMultiValueMap::addAll); } - private ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) { + private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) { return ResponseCookie.from(cookie.getName(), cookie.getValue()) .domain(cookie.getDomain()) .httpOnly(cookie.isHttpOnly()) @@ -113,12 +97,12 @@ private ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String same .build(); } - @Override - public Flux getBody() { - return JdkFlowAdapter.flowPublisherToFlux(this.response.body()) + private static Flux adaptBody(HttpResponse>> response, DataBufferFactory bufferFactory) { + return JdkFlowAdapter.flowPublisherToFlux(response.body()) .flatMapIterable(Function.identity()) - .map(this.bufferFactory::wrap) - .doOnDiscard(DataBuffer.class, DataBufferUtils::release); + .map(bufferFactory::wrap) + .doOnDiscard(DataBuffer.class, DataBufferUtils::release) + .cache(0); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java index 7d0e4e73190c..89a47fd56a31 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,8 +21,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.reactive.client.ReactiveResponse; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; @@ -42,49 +42,37 @@ * @see * Jetty ReactiveStreams HttpClient */ -class JettyClientHttpResponse implements ClientHttpResponse { +class JettyClientHttpResponse extends AbstractClientHttpResponse { private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); - private final ReactiveResponse reactiveResponse; + public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux content) { - private final Flux content; - - private final HttpHeaders headers; - - - public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Publisher content) { - this.reactiveResponse = reactiveResponse; - this.content = Flux.from(content); - - MultiValueMap headers = new JettyHeadersAdapter(reactiveResponse.getHeaders()); - this.headers = HttpHeaders.readOnlyHttpHeaders(headers); + super(HttpStatusCode.valueOf(reactiveResponse.getStatus()), + adaptHeaders(reactiveResponse), + adaptCookies(reactiveResponse), + content); } - - @Override - public HttpStatusCode getStatusCode() { - return HttpStatusCode.valueOf(this.reactiveResponse.getStatus()); + private static HttpHeaders adaptHeaders(ReactiveResponse response) { + MultiValueMap headers = new JettyHeadersAdapter(response.getHeaders()); + return HttpHeaders.readOnlyHttpHeaders(headers); } - - @Override - public MultiValueMap getCookies() { + private static MultiValueMap adaptCookies(ReactiveResponse response) { MultiValueMap result = new LinkedMultiValueMap<>(); - List cookieHeader = getHeaders().get(HttpHeaders.SET_COOKIE); - if (cookieHeader != null) { - cookieHeader.forEach(header -> - HttpCookie.parse(header).forEach(cookie -> result.add(cookie.getName(), + List cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE); + cookieHeaders.forEach(header -> + HttpCookie.parse(header.getValue()).forEach(cookie -> result.add(cookie.getName(), ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) .domain(cookie.getDomain()) .path(cookie.getPath()) .maxAge(cookie.getMaxAge()) .secure(cookie.getSecure()) .httpOnly(cookie.isHttpOnly()) - .sameSite(parseSameSite(header)) + .sameSite(parseSameSite(header.getValue())) .build())) ); - } return CollectionUtils.unmodifiableMultiValueMap(result); } @@ -94,15 +82,4 @@ private static String parseSameSite(String headerValue) { return (matcher.matches() ? matcher.group(1) : null); } - - @Override - public Flux getBody() { - return this.content; - } - - @Override - public HttpHeaders getHeaders() { - return this.headers; - } - } 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 a55db81ffa32..2062e097d574 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 @@ -481,7 +481,7 @@ void retrieveJsonNull(ClientHttpConnector connector) { .retrieve() .bodyToMono(Map.class); - StepVerifier.create(result).verifyComplete(); + StepVerifier.create(result).expectComplete().verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest // SPR-15946 @@ -808,7 +808,7 @@ void statusHandlerWithErrorBodyTransformation(ClientHttpConnector connector) { MyException error = (MyException) throwable; assertThat(error.getMessage()).isEqualTo("foofoo"); }) - .verify(); + .verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest @@ -850,7 +850,7 @@ void statusHandlerSuppressedErrorSignal(ClientHttpConnector connector) { StepVerifier.create(result) .expectNext("Internal Server error") - .verifyComplete(); + .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { @@ -880,7 +880,7 @@ void statusHandlerSuppressedErrorSignalWithFlux(ClientHttpConnector connector) { StepVerifier.create(result) .expectNext("Internal Server error") - .verifyComplete(); + .expectComplete().verify(Duration.ofSeconds(3)); expectRequestCount(1); expectRequest(request -> { @@ -1038,7 +1038,7 @@ void exchangeForEmptyBodyAsVoidEntity(ClientHttpConnector connector) { StepVerifier.create(result) .assertNext(r -> assertThat(r.getStatusCode().is2xxSuccessful()).isTrue()) - .verifyComplete(); + .expectComplete().verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest @@ -1207,7 +1207,7 @@ void malformedResponseChunksOnBodilessEntity(ClientHttpConnector connector) { WebClientException ex = (WebClientException) throwable; assertThat(ex.getCause()).isInstanceOf(IOException.class); }) - .verify(); + .verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest @@ -1219,7 +1219,7 @@ void malformedResponseChunksOnEntityWithBody(ClientHttpConnector connector) { WebClientException ex = (WebClientException) throwable; assertThat(ex.getCause()).isInstanceOf(IOException.class); }) - .verify(); + .verify(Duration.ofSeconds(3)); } @ParameterizedWebClientTest From b06305e64f2e302d015e93b27761784518e5480d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 24 Jan 2024 22:30:28 +0100 Subject: [PATCH 079/261] Compare qualifier value arrays with equality semantics Closes gh-32106 (cherry picked from commit c5a75219ceb5b5bc6a1afbeddff161d4e3f3a753) --- ...erAnnotationAutowireCandidateResolver.java | 4 +- ...alifierAnnotationAutowireContextTests.java | 113 ++++++++++-------- .../factory/xml/QualifierAnnotationTests.java | 14 ++- 3 files changed, 71 insertions(+), 60 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index cc926c37e5ca..2f0b6bc2a09c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -292,7 +292,7 @@ protected boolean checkQualifier( if (actualValue != null) { actualValue = typeConverter.convertIfNecessary(actualValue, expectedValue.getClass()); } - if (!expectedValue.equals(actualValue)) { + if (!ObjectUtils.nullSafeEquals(expectedValue, actualValue)) { return false; } } diff --git a/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java index f126d20473f4..a1cceb0a798c 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireContextTests.java @@ -63,12 +63,12 @@ public void autowiredFieldWithSingleNonQualifiedCandidate() { new RootBeanDefinition(QualifiedFieldTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -81,12 +81,13 @@ public void autowiredMethodParameterWithSingleNonQualifiedCandidate() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @@ -100,9 +101,10 @@ public void autowiredConstructorArgumentWithSingleNonQualifiedCandidate() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @Test @@ -205,12 +207,13 @@ public void autowiredFieldWithMultipleNonQualifiedCandidates() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -227,12 +230,13 @@ public void autowiredMethodParameterWithMultipleNonQualifiedCandidates() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedMethodParameterTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -249,9 +253,10 @@ public void autowiredConstructorArgumentWithMultipleNonQualifiedCandidates() { context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @Test @@ -374,12 +379,13 @@ public void autowiredFieldDoesNotResolveCandidateWithDefaultValueAndConflictingV context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithDefaultValueTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -451,12 +457,13 @@ public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndConflictin context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -507,12 +514,13 @@ public void autowiredFieldDoesNotResolveWithMultipleQualifierValuesAndMultipleMa context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedFieldWithMultipleAttributesTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .satisfies(ex -> { - assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); - assertThat(ex.getBeanName()).isEqualTo("autowired"); - }); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> { + assertThat(ex.getRootCause()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(ex.getBeanName()).isEqualTo("autowired"); + }); } @Test @@ -574,9 +582,10 @@ public void autowiredFieldDoesNotResolveWithBaseQualifierAndNonDefaultValueAndMu context.registerBeanDefinition("autowired", new RootBeanDefinition(QualifiedConstructorArgumentWithBaseQualifierNonDefaultValueTestBean.class)); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy( - context::refresh) - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); + + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(context::refresh) + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("autowired")); } @@ -752,7 +761,7 @@ public DefaultValueQualifiedPerson(String name) { @Qualifier @interface TestQualifierWithMultipleAttributes { - String value() default "default"; + String[] value() default "default"; int number(); } diff --git a/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java b/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java index 49236e0422a7..0e966b1742ac 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/xml/QualifierAnnotationTests.java @@ -58,9 +58,10 @@ public void testNonQualifiedFieldFails() { BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); context.registerSingleton("testBean", NonQualifiedTestBean.class); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .withMessageContaining("found 6"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .withMessageContaining("found 6"); } @Test @@ -191,9 +192,10 @@ public void testQualifiedByAttributesFailsWithoutCustomQualifierRegistered() { BeanDefinitionReader reader = new XmlBeanDefinitionReader(context); reader.loadBeanDefinitions(CONFIG_LOCATION); context.registerSingleton("testBean", QualifiedByAttributesTestBean.class); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - context::refresh) - .withMessageContaining("found 6"); + + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(context::refresh) + .withMessageContaining("found 6"); } @Test From d50d4a909259f23466be5c14891b488aa1ff37fc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 24 Jan 2024 22:44:29 +0100 Subject: [PATCH 080/261] Polishing --- .../aop/scope/ScopedProxyTests.java | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java b/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java index 1f215849aa09..9e2e1bb87184 100644 --- a/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/scope/ScopedProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -39,7 +39,7 @@ * @author Juergen Hoeller * @author Chris Beams */ -public class ScopedProxyTests { +class ScopedProxyTests { private static final Class CLASS = ScopedProxyTests.class; private static final String CLASSNAME = CLASS.getSimpleName(); @@ -51,27 +51,24 @@ public class ScopedProxyTests { @Test // SPR-2108 - public void testProxyAssignable() throws Exception { + void testProxyAssignable() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(MAP_CONTEXT); Object baseMap = bf.getBean("singletonMap"); - boolean condition = baseMap instanceof Map; - assertThat(condition).isTrue(); + assertThat(baseMap instanceof Map).isTrue(); } @Test - public void testSimpleProxy() throws Exception { + void testSimpleProxy() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(MAP_CONTEXT); Object simpleMap = bf.getBean("simpleMap"); - boolean condition1 = simpleMap instanceof Map; - assertThat(condition1).isTrue(); - boolean condition = simpleMap instanceof HashMap; - assertThat(condition).isTrue(); + assertThat(simpleMap instanceof Map).isTrue(); + assertThat(simpleMap instanceof HashMap).isTrue(); } @Test - public void testScopedOverride() throws Exception { + void testScopedOverride() { GenericApplicationContext ctx = new GenericApplicationContext(); new XmlBeanDefinitionReader(ctx).loadBeanDefinitions(OVERRIDE_CONTEXT); SimpleMapScope scope = new SimpleMapScope(); @@ -87,7 +84,7 @@ public void testScopedOverride() throws Exception { } @Test - public void testJdkScopedProxy() throws Exception { + void testJdkScopedProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(TESTBEAN_CONTEXT); bf.setSerializationId("X"); @@ -97,8 +94,7 @@ public void testJdkScopedProxy() throws Exception { ITestBean bean = (ITestBean) bf.getBean("testBean"); assertThat(bean).isNotNull(); assertThat(AopUtils.isJdkDynamicProxy(bean)).isTrue(); - boolean condition1 = bean instanceof ScopedObject; - assertThat(condition1).isTrue(); + assertThat(bean instanceof ScopedObject).isTrue(); ScopedObject scoped = (ScopedObject) bean; assertThat(scoped.getTargetObject().getClass()).isEqualTo(TestBean.class); bean.setAge(101); @@ -110,8 +106,7 @@ public void testJdkScopedProxy() throws Exception { assertThat(deserialized).isNotNull(); assertThat(AopUtils.isJdkDynamicProxy(deserialized)).isTrue(); assertThat(bean.getAge()).isEqualTo(101); - boolean condition = deserialized instanceof ScopedObject; - assertThat(condition).isTrue(); + assertThat(deserialized instanceof ScopedObject).isTrue(); ScopedObject scopedDeserialized = (ScopedObject) deserialized; assertThat(scopedDeserialized.getTargetObject().getClass()).isEqualTo(TestBean.class); @@ -119,7 +114,7 @@ public void testJdkScopedProxy() throws Exception { } @Test - public void testCglibScopedProxy() throws Exception { + void testCglibScopedProxy() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions(LIST_CONTEXT); bf.setSerializationId("Y"); @@ -128,8 +123,7 @@ public void testCglibScopedProxy() throws Exception { TestBean tb = (TestBean) bf.getBean("testBean"); assertThat(AopUtils.isCglibProxy(tb.getFriends())).isTrue(); - boolean condition1 = tb.getFriends() instanceof ScopedObject; - assertThat(condition1).isTrue(); + assertThat(tb.getFriends() instanceof ScopedObject).isTrue(); ScopedObject scoped = (ScopedObject) tb.getFriends(); assertThat(scoped.getTargetObject().getClass()).isEqualTo(ArrayList.class); tb.getFriends().add("myFriend"); @@ -137,12 +131,11 @@ public void testCglibScopedProxy() throws Exception { assertThat(scope.getMap().containsKey("scopedTarget.scopedList")).isTrue(); assertThat(scope.getMap().get("scopedTarget.scopedList").getClass()).isEqualTo(ArrayList.class); - ArrayList deserialized = (ArrayList) SerializationTestUtils.serializeAndDeserialize(tb.getFriends()); + ArrayList deserialized = (ArrayList) SerializationTestUtils.serializeAndDeserialize(tb.getFriends()); assertThat(deserialized).isNotNull(); assertThat(AopUtils.isCglibProxy(deserialized)).isTrue(); - assertThat(deserialized.contains("myFriend")).isTrue(); - boolean condition = deserialized instanceof ScopedObject; - assertThat(condition).isTrue(); + assertThat(deserialized).contains("myFriend"); + assertThat(deserialized instanceof ScopedObject).isTrue(); ScopedObject scopedDeserialized = (ScopedObject) deserialized; assertThat(scopedDeserialized.getTargetObject().getClass()).isEqualTo(ArrayList.class); From f262046ef9785bffe3d6ccd93eecb426d9e69c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 29 Jan 2024 00:03:40 +0800 Subject: [PATCH 081/261] Update basics.adoc Closes gh-32151 --- framework-docs/modules/ROOT/pages/core/beans/basics.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc index 57e51a6f1e9f..2c208b609e95 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc @@ -141,7 +141,7 @@ Kotlin:: ==== After you learn about Spring's IoC container, you may want to know more about Spring's `Resource` abstraction (as described in -xref:web/webflux-webclient/client-builder.adoc#webflux-client-builder-reactor-resources[Resources]) +xref:core/resources.adoc[Resources]) which provides a convenient mechanism for reading an InputStream from locations defined in a URI syntax. In particular, `Resource` paths are used to construct applications contexts, as described in xref:core/resources.adoc#resources-app-ctx[Application Contexts and Resource Paths]. From 4910c217dc897edd81f086941c7d7941afd73e8e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 29 Jan 2024 13:02:43 +0100 Subject: [PATCH 082/261] Explicit documentation note on cron-vs-quartz parsing convention Closes gh-32128 (cherry picked from commit a738e4d5fd58b1c8b06c63fb1efba4b5eb2bcaae) --- .../scheduling/support/CronExpression.java | 11 +++- .../scheduling/support/CronField.java | 19 ++++-- .../scheduling/support/CronTrigger.java | 10 ++- .../scheduling/support/QuartzCronField.java | 61 +++++++++---------- .../support/BitsCronFieldTests.java | 19 ++++-- .../support/QuartzCronFieldTests.java | 37 +++++++++++ 6 files changed, 108 insertions(+), 49 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java index 5d58384a3943..fdc7cb96dc28 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -29,9 +29,14 @@ * crontab expression * that can calculate the next time it matches. * - *

    {@code CronExpression} instances are created through - * {@link #parse(String)}; the next match is determined with - * {@link #next(Temporal)}. + *

    {@code CronExpression} instances are created through {@link #parse(String)}; + * the next match is determined with {@link #next(Temporal)}. + * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Arjen Poutsma * @since 5.3 diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index 99d940613e5c..3369f62bafca 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -31,15 +31,22 @@ * Single field in a cron pattern. Created using the {@code parse*} methods, * main and only entry point is {@link #nextOrSame(Temporal)}. * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. + * * @author Arjen Poutsma * @since 5.3 */ abstract class CronField { - private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", - "OCT", "NOV", "DEC"}; + private static final String[] MONTHS = new String[] + {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; - private static final String[] DAYS = new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; + private static final String[] DAYS = new String[] + {"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; private final Type type; @@ -48,6 +55,7 @@ protected CronField(Type type) { this.type = type; } + /** * Return a {@code CronField} enabled for 0 nanoseconds. */ @@ -169,6 +177,7 @@ protected static > T cast(Temporal te * day-of-month, month, day-of-week. */ protected enum Type { + NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS), SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND), MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), @@ -184,14 +193,12 @@ protected enum Type { private final ChronoField[] lowerOrders; - Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) { this.field = field; this.higherOrder = higherOrder; this.lowerOrders = lowerOrders; } - /** * Return the value of this type for the given temporal. * @return the value of this type diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java index 88228a3c0835..c9ebff591b7f 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -27,8 +27,14 @@ import org.springframework.util.Assert; /** - * {@link Trigger} implementation for cron expressions. - * Wraps a {@link CronExpression}. + * {@link Trigger} implementation for cron expressions. Wraps a + * {@link CronExpression} which parses according to common crontab conventions. + * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. * * @author Juergen Hoeller * @author Arjen Poutsma diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java index d1f0a547071e..f9893f84492f 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,10 +30,15 @@ /** * Extension of {@link CronField} for * Quartz-specific fields. - * - *

    Created using the {@code parse*} methods, uses a {@link TemporalAdjuster} + * Created using the {@code parse*} methods, uses a {@link TemporalAdjuster} * internally. * + *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows + * common cron conventions in every other respect, including 0-6 for SUN-SAT + * (plus 7 for SUN as well). Note that Quartz deviates from the day-of-week + * convention in cron through 1-7 for SUN-SAT whereas Spring strictly follows + * cron even in combination with the optional Quartz-specific L/# expressions. + * * @author Arjen Poutsma * @since 5.3 */ @@ -61,8 +66,9 @@ private QuartzCronField(Type type, Type rollForwardType, TemporalAdjuster adjust this.rollForwardType = rollForwardType; } + /** - * Returns whether the given value is a Quartz day-of-month field. + * Determine whether the given value is a Quartz day-of-month field. */ public static boolean isQuartzDaysOfMonthField(String value) { return value.contains("L") || value.contains("W"); @@ -80,14 +86,14 @@ public static QuartzCronField parseDaysOfMonth(String value) { if (idx != 0) { throw new IllegalArgumentException("Unrecognized characters before 'L' in '" + value + "'"); } - else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" + else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" adjuster = lastWeekdayOfMonth(); } else { - if (value.length() == 1) { // "L" + if (value.length() == 1) { // "L" adjuster = lastDayOfMonth(); } - else { // "L-[0-9]+" + else { // "L-[0-9]+" int offset = Integer.parseInt(value, idx + 1, value.length(), 10); if (offset >= 0) { throw new IllegalArgumentException("Offset '" + offset + " should be < 0 '" + value + "'"); @@ -105,7 +111,7 @@ else if (value.length() == 2 && value.charAt(1) == 'W') { // "LW" else if (idx != value.length() - 1) { throw new IllegalArgumentException("Unrecognized characters after 'W' in '" + value + "'"); } - else { // "[0-9]+W" + else { // "[0-9]+W" int dayOfMonth = Integer.parseInt(value, 0, idx, 10); dayOfMonth = Type.DAY_OF_MONTH.checkValidValue(dayOfMonth); TemporalAdjuster adjuster = weekdayNearestTo(dayOfMonth); @@ -116,7 +122,7 @@ else if (idx != value.length() - 1) { } /** - * Returns whether the given value is a Quartz day-of-week field. + * Determine whether the given value is a Quartz day-of-week field. */ public static boolean isQuartzDaysOfWeekField(String value) { return value.contains("L") || value.contains("#"); @@ -138,7 +144,7 @@ public static QuartzCronField parseDaysOfWeek(String value) { if (idx == 0) { throw new IllegalArgumentException("No day-of-week before 'L' in '" + value + "'"); } - else { // "[0-7]L" + else { // "[0-7]L" DayOfWeek dayOfWeek = parseDayOfWeek(value.substring(0, idx)); adjuster = lastInMonth(dayOfWeek); } @@ -160,7 +166,6 @@ else if (idx == value.length() - 1) { throw new IllegalArgumentException("Ordinal '" + ordinal + "' in '" + value + "' must be positive number "); } - TemporalAdjuster adjuster = dayOfWeekInMonth(ordinal, dayOfWeek); return new QuartzCronField(Type.DAY_OF_WEEK, Type.DAY_OF_MONTH, adjuster, value); } @@ -170,14 +175,13 @@ else if (idx == value.length() - 1) { private static DayOfWeek parseDayOfWeek(String value) { int dayOfWeek = Integer.parseInt(value); if (dayOfWeek == 0) { - dayOfWeek = 7; // cron is 0 based; java.time 1 based + dayOfWeek = 7; // cron is 0 based; java.time 1 based } try { return DayOfWeek.of(dayOfWeek); } catch (DateTimeException ex) { - String msg = ex.getMessage() + " '" + value + "'"; - throw new IllegalArgumentException(msg, ex); + throw new IllegalArgumentException(ex.getMessage() + " '" + value + "'", ex); } } @@ -216,10 +220,10 @@ private static TemporalAdjuster lastWeekdayOfMonth() { Temporal lastDom = adjuster.adjustInto(temporal); Temporal result; int dow = lastDom.get(ChronoField.DAY_OF_WEEK); - if (dow == 6) { // Saturday + if (dow == 6) { // Saturday result = lastDom.minus(1, ChronoUnit.DAYS); } - else if (dow == 7) { // Sunday + else if (dow == 7) { // Sunday result = lastDom.minus(2, ChronoUnit.DAYS); } else { @@ -256,10 +260,10 @@ private static TemporalAdjuster weekdayNearestTo(int dayOfMonth) { int current = Type.DAY_OF_MONTH.get(temporal); DayOfWeek dayOfWeek = DayOfWeek.from(temporal); - if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday - (dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before - (dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after - (dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd + if ((current == dayOfMonth && isWeekday(dayOfWeek)) || // dayOfMonth is a weekday + (dayOfWeek == DayOfWeek.FRIDAY && current == dayOfMonth - 1) || // dayOfMonth is a Saturday, so Friday before + (dayOfWeek == DayOfWeek.MONDAY && current == dayOfMonth + 1) || // dayOfMonth is a Sunday, so Monday after + (dayOfWeek == DayOfWeek.MONDAY && dayOfMonth == 1 && current == 3)) { // dayOfMonth is Saturday 1st, so Monday 3rd return temporal; } int count = 0; @@ -357,26 +361,19 @@ private > T adjust(T temporal) { @Override - public int hashCode() { - return this.value.hashCode(); + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof QuartzCronField that && + type() == that.type() && this.value.equals(that.value))); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (!(o instanceof QuartzCronField other)) { - return false; - } - return type() == other.type() && - this.value.equals(other.value); + public int hashCode() { + return this.value.hashCode(); } @Override public String toString() { return type() + " '" + this.value + "'"; - } } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java index 961312a64ff8..c114ec5b52a2 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java @@ -35,13 +35,16 @@ class BitsCronFieldTests { @Test void parse() { assertThat(BitsCronField.parseSeconds("42")).has(clearRange(0, 41)).has(set(42)).has(clearRange(43, 59)); - assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)).has(setRange(8, 12)).has(clearRange(13,59)); - assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)).has(clear(58)).has(set(59)); + assertThat(BitsCronField.parseSeconds("0-4,8-12")).has(setRange(0, 4)).has(clearRange(5,7)) + .has(setRange(8, 12)).has(clearRange(13,59)); + assertThat(BitsCronField.parseSeconds("57/2")).has(clearRange(0, 56)).has(set(57)) + .has(clear(58)).has(set(59)); assertThat(BitsCronField.parseMinutes("30")).has(set(30)).has(clearRange(1, 29)).has(clearRange(31, 59)); assertThat(BitsCronField.parseHours("23")).has(set(23)).has(clearRange(0, 23)); - assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)).has(clear(1,3,5,7,9,11,13,15,17,19,21,23)); + assertThat(BitsCronField.parseHours("0-23/2")).has(set(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22)) + .has(clear(1,3,5,7,9,11,13,15,17,19,21,23)); assertThat(BitsCronField.parseDaysOfMonth("1")).has(set(1)).has(clearRange(2, 31)); @@ -49,13 +52,16 @@ void parse() { assertThat(BitsCronField.parseDaysOfWeek("0")).has(set(7, 7)).has(clearRange(0, 6)); - assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5)).has(clear(6)).has(set(7)); + assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5)) + .has(clear(6)).has(set(7)); } @Test void parseLists() { - assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15)).has(clearRange(31, 59)); - assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0)).has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59)); + assertThat(BitsCronField.parseSeconds("15,30")).has(set(15, 30)).has(clearRange(1, 15)) + .has(clearRange(31, 59)); + assertThat(BitsCronField.parseMinutes("1,2,5,9")).has(set(1, 2, 5, 9)).has(clear(0)) + .has(clearRange(3, 4)).has(clearRange(6, 8)).has(clearRange(10, 59)); assertThat(BitsCronField.parseHours("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 23)); assertThat(BitsCronField.parseDaysOfMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 31)); assertThat(BitsCronField.parseMonth("1,2,3")).has(set(1, 2, 3)).has(clearRange(4, 12)); @@ -107,6 +113,7 @@ void names() { .has(clear(0)).has(setRange(1, 7)); } + private static Condition set(int... indices) { return new Condition<>(String.format("set bits %s", Arrays.toString(indices))) { @Override diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java index ec56f366b5b2..cf9a5989f686 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java @@ -28,6 +28,7 @@ * Unit tests for {@link QuartzCronField}. * * @author Arjen Poutsma + * @author Juergen Hoeller */ class QuartzCronFieldTests { @@ -71,6 +72,42 @@ void lastDayOfWeekOffset() { assertThat(field.nextOrSame(last)).isEqualTo(expected); } + @Test + void dayOfWeek_0(){ + QuartzCronField field = QuartzCronField.parseDaysOfWeek("0#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 21); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_1(){ + QuartzCronField field = QuartzCronField.parseDaysOfWeek("1#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 15); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_2(){ + QuartzCronField field = QuartzCronField.parseDaysOfWeek("2#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 16); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + + @Test + void dayOfWeek_7() { + QuartzCronField field = QuartzCronField.parseDaysOfWeek("7#3"); + + LocalDate last = LocalDate.of(2024, 1, 1); + LocalDate expected = LocalDate.of(2024, 1, 21); + assertThat(field.nextOrSame(last)).isEqualTo(expected); + } + @Test void invalidValues() { assertThatIllegalArgumentException().isThrownBy(() -> QuartzCronField.parseDaysOfMonth("")); From 0909161ba1403205804cba0176831d4a968c56d0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 29 Jan 2024 14:19:48 +0100 Subject: [PATCH 083/261] Polishing --- .../support/DefaultLifecycleProcessor.java | 10 ++++-- .../ExecutorConfigurationSupport.java | 10 +++--- .../support/BitsCronFieldTests.java | 3 +- .../support/QuartzCronFieldTests.java | 6 +++- .../GenericTableMetaDataProvider.java | 8 ++--- .../core/metadata/TableMetaDataContext.java | 36 +++++++++---------- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index 44dca4ad5ed8..1cba2713651c 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -45,7 +45,11 @@ import org.springframework.util.Assert; /** - * Default implementation of the {@link LifecycleProcessor} strategy. + * Spring's default implementation of the {@link LifecycleProcessor} strategy. + * + *

    Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in + * groups for specific phases, on startup/shutdown as well as for explicit start/stop + * interactions on a {@link org.springframework.context.ConfigurableApplicationContext}. * * @author Mark Fisher * @author Juergen Hoeller @@ -314,6 +318,8 @@ protected int getPhase(Lifecycle bean) { /** * Helper class for maintaining a group of Lifecycle beans that should be started * and stopped together based on their 'phase' value (or the default value of 0). + * The group is expected to be created in an ad-hoc fashion and group members are + * expected to always have the same 'phase' value. */ private class LifecycleGroup { diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java index ae4d3a1e34ab..0fae42b440d8 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -73,7 +73,7 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac /** * Set the ThreadFactory to use for the ExecutorService's thread pool. - * Default is the underlying ExecutorService's default thread factory. + * The default is the underlying ExecutorService's default thread factory. *

    In a Jakarta EE or other managed environment with JSR-236 support, * consider specifying a JNDI-located ManagedThreadFactory: by default, * to be found at "java:comp/DefaultManagedThreadFactory". @@ -108,7 +108,7 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec /** * Set whether to wait for scheduled tasks to complete on shutdown, * not interrupting running tasks and executing all tasks in the queue. - *

    Default is {@code false}, shutting down immediately through interrupting + *

    The default is {@code false}, shutting down immediately through interrupting * ongoing tasks and clearing the queue. Switch this flag to {@code true} if * you prefer fully completed tasks at the expense of a longer shutdown phase. *

    Note that Spring's container shutdown continues while ongoing tasks @@ -119,6 +119,8 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec * property instead of or in addition to this property. * @see java.util.concurrent.ExecutorService#shutdown() * @see java.util.concurrent.ExecutorService#shutdownNow() + * @see #shutdown() + * @see #setAwaitTerminationSeconds */ public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) { this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown; @@ -237,7 +239,7 @@ public void shutdown() { } /** - * Cancel the given remaining task which never commended execution, + * Cancel the given remaining task which never commenced execution, * as returned from {@link ExecutorService#shutdownNow()}. * @param task the task to cancel (typically a {@link RunnableFuture}) * @since 5.0.5 diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java index c114ec5b52a2..393a939c05fd 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/BitsCronFieldTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -51,7 +51,6 @@ void parse() { assertThat(BitsCronField.parseMonth("1")).has(set(1)).has(clearRange(2, 12)); assertThat(BitsCronField.parseDaysOfWeek("0")).has(set(7, 7)).has(clearRange(0, 6)); - assertThat(BitsCronField.parseDaysOfWeek("7-5")).has(clear(0)).has(setRange(1, 5)) .has(clear(6)).has(set(7)); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java index cf9a5989f686..14c5907af48a 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/QuartzCronFieldTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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,6 +74,7 @@ void lastDayOfWeekOffset() { @Test void dayOfWeek_0(){ + // third Sunday (0) of the month QuartzCronField field = QuartzCronField.parseDaysOfWeek("0#3"); LocalDate last = LocalDate.of(2024, 1, 1); @@ -83,6 +84,7 @@ void dayOfWeek_0(){ @Test void dayOfWeek_1(){ + // third Monday (1) of the month QuartzCronField field = QuartzCronField.parseDaysOfWeek("1#3"); LocalDate last = LocalDate.of(2024, 1, 1); @@ -92,6 +94,7 @@ void dayOfWeek_1(){ @Test void dayOfWeek_2(){ + // third Tuesday (2) of the month QuartzCronField field = QuartzCronField.parseDaysOfWeek("2#3"); LocalDate last = LocalDate.of(2024, 1, 1); @@ -101,6 +104,7 @@ void dayOfWeek_2(){ @Test void dayOfWeek_7() { + // third Sunday (7 as alternative to 0) of the month QuartzCronField field = QuartzCronField.parseDaysOfWeek("7#3"); LocalDate last = LocalDate.of(2024, 1, 1); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java index 85c3bc65fcc6..5625be1e7483 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -158,7 +158,7 @@ public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQL } catch (SQLException ex) { if (logger.isWarnEnabled()) { - logger.warn("Error retrieving 'DatabaseMetaData.getGeneratedKeys': " + ex.getMessage()); + logger.warn("Error retrieving 'DatabaseMetaData.supportsGetGeneratedKeys': " + ex.getMessage()); } } try { @@ -290,7 +290,7 @@ public String metaDataSchemaNameToUse(@Nullable String schemaName) { } /** - * Provide access to default schema for subclasses. + * Provide access to the default schema for subclasses. */ @Nullable protected String getDefaultSchema() { @@ -298,7 +298,7 @@ protected String getDefaultSchema() { } /** - * Provide access to version info for subclasses. + * Provide access to the version info for subclasses. */ @Nullable protected String getDatabaseVersion() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java index 6676d9d8df66..89097890d469 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,9 +63,6 @@ public class TableMetaDataContext { @Nullable private String schemaName; - // List of columns objects to be used in this context - private List tableColumns = new ArrayList<>(); - // Should we access insert parameter meta-data info or not private boolean accessTableColumnMetaData = true; @@ -76,6 +73,9 @@ public class TableMetaDataContext { @Nullable private TableMetaDataProvider metaDataProvider; + // List of columns objects to be used in this context + private List tableColumns = new ArrayList<>(); + // Are we using generated key columns private boolean generatedKeyColumnsUsed = false; @@ -139,7 +139,6 @@ public boolean isAccessTableColumnMetaData() { return this.accessTableColumnMetaData; } - /** * Specify whether we should override default for accessing synonyms. */ @@ -266,7 +265,6 @@ public List matchInParameterValuesWithInsertColumns(Map inPar return values; } - /** * Build the insert string based on configuration and meta-data information. * @return the insert string to be used @@ -303,8 +301,8 @@ public String createInsertString(String... generatedKeyNames) { } } else { - String message = "Unable to locate columns for table '" + getTableName() - + "' so an insert statement can't be generated."; + String message = "Unable to locate columns for table '" + getTableName() + + "' so an insert statement can't be generated."; if (isAccessTableColumnMetaData()) { message += " Consider specifying explicit column names -- for example, via SimpleJdbcInsert#usingColumns()."; } @@ -349,26 +347,27 @@ public int[] createInsertTypes() { /** - * Does this database support the JDBC 3.0 feature of retrieving generated keys: - * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + * Does this database support the JDBC feature for retrieving generated keys? + * @see java.sql.DatabaseMetaData#supportsGetGeneratedKeys() */ public boolean isGetGeneratedKeysSupported() { return obtainMetaDataProvider().isGetGeneratedKeysSupported(); } /** - * Does this database support simple query to retrieve generated keys - * when the JDBC 3.0 feature is not supported: - * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + * Does this database support a simple query to retrieve generated keys when + * the JDBC feature for retrieving generated keys is not supported? + * @see #isGetGeneratedKeysSupported() + * @see #getSimpleQueryForGetGeneratedKey(String, String) */ public boolean isGetGeneratedKeysSimulated() { return obtainMetaDataProvider().isGetGeneratedKeysSimulated(); } /** - * Does this database support a simple query to retrieve generated keys - * when the JDBC 3.0 feature is not supported: - * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}? + * Get the simple query to retrieve generated keys when the JDBC feature for + * retrieving generated keys is not supported. + * @see #isGetGeneratedKeysSimulated() */ @Nullable public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { @@ -376,8 +375,9 @@ public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColum } /** - * Is a column name String array for retrieving generated keys supported: - * {@link java.sql.Connection#createStruct(String, Object[])}? + * Does this database support a column name String array for retrieving generated + * keys? + * @see java.sql.Connection#createStruct(String, Object[]) */ public boolean isGeneratedKeysColumnNameArraySupported() { return obtainMetaDataProvider().isGeneratedKeysColumnNameArraySupported(); From bfd3b3ad885cf7d9c1cf64ac6f2c702e6aacdb71 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 29 Jan 2024 16:21:35 +0100 Subject: [PATCH 084/261] Eagerly initialize ZERO_NANOS constant --- .../scheduling/support/BitsCronField.java | 42 +++++++------------ .../scheduling/support/CronField.java | 5 +-- .../scheduling/support/QuartzCronField.java | 2 +- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java index ac877a72a460..cb9ffde0df19 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -33,12 +33,9 @@ */ final class BitsCronField extends CronField { - private static final long MASK = 0xFFFFFFFFFFFFFFFFL; - - - @Nullable - private static BitsCronField zeroNanos = null; + public static BitsCronField ZERO_NANOS = forZeroNanos(); + private static final long MASK = 0xFFFFFFFFFFFFFFFFL; // we store at most 60 bits, for seconds and minutes, so a 64-bit long suffices private long bits; @@ -48,16 +45,14 @@ private BitsCronField(Type type) { super(type); } + /** * Return a {@code BitsCronField} enabled for 0 nanoseconds. */ - public static BitsCronField zeroNanos() { - if (zeroNanos == null) { - BitsCronField field = new BitsCronField(Type.NANO); - field.setBit(0); - zeroNanos = field; - } - return zeroNanos; + private static BitsCronField forZeroNanos() { + BitsCronField field = new BitsCronField(Type.NANO); + field.setBit(0); + return field; } /** @@ -108,7 +103,6 @@ public static BitsCronField parseDaysOfWeek(String value) { return result; } - private static BitsCronField parseDate(String value, BitsCronField.Type type) { if (value.equals("?")) { value = "*"; @@ -174,6 +168,7 @@ private static ValueRange parseRange(String value, Type type) { } } + @Nullable @Override public > T nextOrSame(T temporal) { @@ -217,7 +212,6 @@ private int nextSetBit(int fromIndex) { else { return -1; } - } private void setBits(ValueRange range) { @@ -247,23 +241,19 @@ private void setBit(int index) { } private void clearBit(int index) { - this.bits &= ~(1L << index); + this.bits &= ~(1L << index); } + @Override - public int hashCode() { - return Long.hashCode(this.bits); + public boolean equals(Object other) { + return (this == other || (other instanceof BitsCronField that && + type() == that.type() && this.bits == that.bits)); } @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (!(o instanceof BitsCronField other)) { - return false; - } - return type() == other.type() && this.bits == other.bits; + public int hashCode() { + return Long.hashCode(this.bits); } @Override diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index 3369f62bafca..3124cc25b5e5 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -29,7 +29,7 @@ /** * Single field in a cron pattern. Created using the {@code parse*} methods, - * main and only entry point is {@link #nextOrSame(Temporal)}. + * the main and only entry point is {@link #nextOrSame(Temporal)}. * *

    Supports a Quartz day-of-month/week field with an L/# expression. Follows * common cron conventions in every other respect, including 0-6 for SUN-SAT @@ -60,7 +60,7 @@ protected CronField(Type type) { * Return a {@code CronField} enabled for 0 nanoseconds. */ public static CronField zeroNanos() { - return BitsCronField.zeroNanos(); + return BitsCronField.ZERO_NANOS; } /** @@ -186,7 +186,6 @@ protected enum Type { MONTH(ChronoField.MONTH_OF_YEAR, ChronoUnit.YEARS, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoUnit.WEEKS, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); - private final ChronoField field; private final ChronoUnit higherOrder; diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java index f9893f84492f..288957a99a48 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -336,6 +336,7 @@ private static Temporal rollbackToMidnight(Temporal current, Temporal result) { } } + @Override public > T nextOrSame(T temporal) { T result = adjust(temporal); @@ -352,7 +353,6 @@ public > T nextOrSame(T temporal) { return result; } - @Nullable @SuppressWarnings("unchecked") private > T adjust(T temporal) { From d8d4fa0e2419e67f5288479383256ff1ba1a0a8e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 29 Jan 2024 16:42:32 +0100 Subject: [PATCH 085/261] Polishing --- .../scheduling/support/BitsCronField.java | 3 ++- .../web/client/RestTemplate.java | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java index cb9ffde0df19..3ef274e63c5c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java @@ -29,11 +29,12 @@ * Created using the {@code parse*} methods. * * @author Arjen Poutsma + * @author Juergen Hoeller * @since 5.3 */ final class BitsCronField extends CronField { - public static BitsCronField ZERO_NANOS = forZeroNanos(); + public static final BitsCronField ZERO_NANOS = forZeroNanos(); private static final long MASK = 0xFFFFFFFFFFFFFFFFL; diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index f88c41b7c163..94e06500d0cc 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -367,6 +367,7 @@ public void setObservationConvention(ClientRequestObservationConvention observat this.observationConvention = observationConvention; } + // GET @Override @@ -847,13 +848,14 @@ protected T doExecute(URI url, @Nullable String uriTemplate, @Nullable HttpM request = createRequest(url, method); } catch (IOException ex) { - ResourceAccessException exception = createResourceAccessException(url, method, ex); - throw exception; + throw createResourceAccessException(url, method, ex); } + ClientRequestObservationContext observationContext = new ClientRequestObservationContext(request); observationContext.setUriTemplate(uriTemplate); - Observation observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation(this.observationConvention, - DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).start(); + Observation observation = ClientHttpObservationDocumentation.HTTP_CLIENT_EXCHANGES.observation( + this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, + () -> observationContext, this.observationRegistry).start(); ClientHttpResponse response = null; try (Observation.Scope scope = observation.openScope()){ if (requestCallback != null) { @@ -865,13 +867,13 @@ protected T doExecute(URI url, @Nullable String uriTemplate, @Nullable HttpM return (responseExtractor != null ? responseExtractor.extractData(response) : null); } catch (IOException ex) { - ResourceAccessException exception = createResourceAccessException(url, method, ex); - observation.error(exception); - throw exception; + ResourceAccessException accessEx = createResourceAccessException(url, method, ex); + observation.error(accessEx); + throw accessEx; } - catch (Throwable exc) { - observation.error(exc); - throw exc; + catch (Throwable ex) { + observation.error(ex); + throw ex; } finally { if (response != null) { From 125ebd029ce590aecfca2195498abb0829278378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 30 Jan 2024 15:30:47 +0100 Subject: [PATCH 086/261] Prevent AOT from failing with spring-orm without JPA This commit improves PersistenceManagedTypesBeanRegistrationAotProcessor so that it does not attempt to load JPA classes when checking for the presence of a PersistenceManagedTypes bean. To make it more clear a check on the presence for JPA has been added to prevent the nested classes to be loaded regardless of the presence of the bean. Closes gh-32160 --- ...agedTypesBeanRegistrationAotProcessor.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java index c1a5cf8eae26..da9f52e93f37 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java @@ -67,20 +67,26 @@ */ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { - private static final List> CALLBACK_TYPES = List.of(PreUpdate.class, - PostUpdate.class, PrePersist.class, PostPersist.class, PreRemove.class, PostRemove.class, PostLoad.class); + private static final boolean jpaPresent = ClassUtils.isPresent("jakarta.persistence.Entity", + PersistenceManagedTypesBeanRegistrationAotProcessor.class.getClassLoader()); @Nullable @Override public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { - if (PersistenceManagedTypes.class.isAssignableFrom(registeredBean.getBeanClass())) { - return BeanRegistrationAotContribution.withCustomCodeFragments(codeFragments -> - new JpaManagedTypesBeanRegistrationCodeFragments(codeFragments, registeredBean)); + if (jpaPresent) { + if (PersistenceManagedTypes.class.isAssignableFrom(registeredBean.getBeanClass())) { + return BeanRegistrationAotContribution.withCustomCodeFragments(codeFragments -> + new JpaManagedTypesBeanRegistrationCodeFragments(codeFragments, registeredBean)); + } } return null; } - private static class JpaManagedTypesBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator { + private static final class JpaManagedTypesBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator { + + private static final List> CALLBACK_TYPES = List.of(PreUpdate.class, + PostUpdate.class, PrePersist.class, PostPersist.class, PreRemove.class, PostRemove.class, PostLoad.class); + private static final ParameterizedTypeName LIST_OF_STRINGS_TYPE = ParameterizedTypeName.get(List.class, String.class); @@ -88,7 +94,7 @@ private static class JpaManagedTypesBeanRegistrationCodeFragments extends BeanRe private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); - public JpaManagedTypesBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments, + private JpaManagedTypesBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments, RegisteredBean registeredBean) { super(codeFragments); this.registeredBean = registeredBean; From 72835f10b9b07921978da419abf2a182abf67967 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 1 Feb 2024 14:58:13 +0100 Subject: [PATCH 087/261] Polishing --- .../org/springframework/core/io/Resource.java | 5 ++--- ...ractLobCreatingPreparedStatementCallback.java | 6 +++--- .../AbstractLobStreamingResultSetExtractor.java | 16 ++++++++-------- .../jdbc/core/support/SqlLobValue.java | 4 ++-- .../jdbc/support/xml/SqlXmlValue.java | 6 ++---- .../converter/StringHttpMessageConverter.java | 7 +++---- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/io/Resource.java b/spring-core/src/main/java/org/springframework/core/io/Resource.java index a7a1f1dc43bc..91458934fed3 100644 --- a/spring-core/src/main/java/org/springframework/core/io/Resource.java +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -152,8 +152,7 @@ default byte[] getContentAsByteArray() throws IOException { } /** - * Returns the contents of this resource as a string, using the specified - * charset. + * Return the contents of this resource as a string, using the specified charset. * @param charset the charset to use for decoding * @return the contents of this resource as a {@code String} * @throws java.io.FileNotFoundException if the resource cannot be resolved as diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java index 42f415cba557..99135870c6c9 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobCreatingPreparedStatementCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -45,8 +45,8 @@ * lobCreator.setBlobAsBinaryStream(ps, 2, contentStream, contentLength); * lobCreator.setClobAsString(ps, 3, description); * } - * } - * ); + * }); + * * * @author Juergen Hoeller * @since 1.0.2 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java index 5427df20526c..7b6de1078e88 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -41,13 +41,13 @@ * final LobHandler lobHandler = new DefaultLobHandler(); // reusable object * * jdbcTemplate.query( - * "SELECT content FROM imagedb WHERE image_name=?", new Object[] {name}, - * new AbstractLobStreamingResultSetExtractor() { - * public void streamData(ResultSet rs) throws SQLException, IOException { - * FileCopyUtils.copy(lobHandler.getBlobAsBinaryStream(rs, 1), contentStream); - * } - * } - * ); + * "SELECT content FROM imagedb WHERE image_name=?", new Object[] {name}, + * new AbstractLobStreamingResultSetExtractor() { + * public void streamData(ResultSet rs) throws SQLException, IOException { + * FileCopyUtils.copy(lobHandler.getBlobAsBinaryStream(rs, 1), contentStream); + * } + * }); + * * * @author Juergen Hoeller * @since 1.0.2 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java index 621921436a08..02543c84c6d7 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -209,7 +209,7 @@ else if (this.content instanceof Reader reader) { } /** - * Close the LobCreator, if any. + * Close the LobCreator. */ @Override public void cleanup() { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java index 3d45d467c28e..d662a9271ae3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2024 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,9 +20,7 @@ /** * Subinterface of {@link org.springframework.jdbc.support.SqlValue} - * that supports passing in XML data to specified column and adds a - * cleanup callback, to be invoked after the value has been set and - * the corresponding statement has been executed. + * that specifically indicates passing in XML data to a specified column. * * @author Thomas Risberg * @since 2.5.5 diff --git a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java index 8b8793e277eb..6da38db05bfa 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,10 +106,9 @@ protected Long getContentLength(String str, @Nullable MediaType contentType) { @Override protected void addDefaultHeaders(HttpHeaders headers, String s, @Nullable MediaType type) throws IOException { if (headers.getContentType() == null ) { - if (type != null && type.isConcrete() && - (type.isCompatibleWith(MediaType.APPLICATION_JSON) || + if (type != null && type.isConcrete() && (type.isCompatibleWith(MediaType.APPLICATION_JSON) || type.isCompatibleWith(APPLICATION_PLUS_JSON))) { - // Prevent charset parameter for JSON.. + // Prevent charset parameter for JSON. headers.setContentType(type); } } From 55717adf8830e3a5c69a2dad78b6d368dad020aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sat, 3 Feb 2024 11:36:40 +0100 Subject: [PATCH 088/261] Upgrade to Gradle 8.6 Closes gh-32193 --- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e0930b89..a80b22ce5cff 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85beecd..7101f8e4676f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail From 74bb42b78f91fd409eb40b0d8bed6088935e915e Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Fri, 2 Feb 2024 10:13:13 -0800 Subject: [PATCH 089/261] Optimize Map methods in ServletAttributesMap ServletAttributesMap inherited default implementations of the size and isEmpty methods from AbstractMap which delegates to the Set returned by entrySet. ServletAttributesMap's entrySet method made this fairly expensive, since it would copy the attributes to a List, then use a Stream to build the Set. To avoid the cost, add implementations of isEmpty / size that don't need to call entrySet at all. Additionally, change entrySet to return a Set view that simply lazily delegates to the underlying servlet request for iteration. Closes gh-32197 --- .../function/DefaultServerRequest.java | 98 +++++++++++++++++-- .../function/DefaultServerRequestTests.java | 44 ++++++++- 2 files changed, 132 insertions(+), 10 deletions(-) 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 a2223c29ea56..4775d37f2bcb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -26,10 +26,13 @@ import java.security.Principal; import java.time.Instant; import java.util.AbstractMap; +import java.util.AbstractSet; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -55,6 +58,7 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; @@ -73,6 +77,7 @@ * * @author Arjen Poutsma * @author Sam Brannen + * @author Patrick Strawderman * @since 5.2 */ class DefaultServerRequest implements ServerRequest { @@ -433,18 +438,77 @@ public boolean containsKey(Object key) { @Override public void clear() { - List attributeNames = Collections.list(this.servletRequest.getAttributeNames()); - attributeNames.forEach(this.servletRequest::removeAttribute); + this.servletRequest.getAttributeNames().asIterator().forEachRemaining(this.servletRequest::removeAttribute); } @Override public Set> entrySet() { - return Collections.list(this.servletRequest.getAttributeNames()).stream() - .map(name -> { - Object value = this.servletRequest.getAttribute(name); - return new SimpleImmutableEntry<>(name, value); - }) - .collect(Collectors.toSet()); + return new AbstractSet<>() { + @Override + public Iterator> iterator() { + return new Iterator<>() { + + private final Iterator attributes = ServletAttributesMap.this.servletRequest.getAttributeNames().asIterator(); + + @Override + public boolean hasNext() { + return this.attributes.hasNext(); + } + + @Override + public Entry next() { + String attribute = this.attributes.next(); + Object value = ServletAttributesMap.this.servletRequest.getAttribute(attribute); + return new SimpleImmutableEntry<>(attribute, value); + } + }; + } + + @Override + public boolean isEmpty() { + return ServletAttributesMap.this.isEmpty(); + } + + @Override + public int size() { + return ServletAttributesMap.this.size(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry entry)) { + return false; + } + String attribute = (String) entry.getKey(); + Object value = ServletAttributesMap.this.servletRequest.getAttribute(attribute); + return value != null && value.equals(entry.getValue()); + } + + @Override + public boolean addAll(@NonNull Collection> c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(@NonNull Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + }; } @Override @@ -467,6 +531,22 @@ public Object remove(Object key) { this.servletRequest.removeAttribute(name); return value; } + + @Override + public int size() { + Enumeration attributes = this.servletRequest.getAttributeNames(); + int size = 0; + while (attributes.hasMoreElements()) { + size++; + attributes.nextElement(); + } + return size; + } + + @Override + public boolean isEmpty() { + return !this.servletRequest.getAttributeNames().hasMoreElements(); + } } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java index 30707f1a8ec4..bce1120ddcd5 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java @@ -27,10 +27,12 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; +import java.util.Set; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Part; @@ -61,8 +63,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** + * Tests for {@link DefaultServerRequest}. * @author Arjen Poutsma - * @since 5.1 */ class DefaultServerRequestTests { @@ -114,6 +116,46 @@ void attribute() { assertThat(request.attribute("foo")).contains("bar"); } + @Test + void attributes() { + MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); + servletRequest.setAttribute("foo", "bar"); + servletRequest.setAttribute("baz", "qux"); + + DefaultServerRequest request = new DefaultServerRequest(servletRequest, this.messageConverters); + + Map attributesMap = request.attributes(); + assertThat(attributesMap).isNotEmpty(); + assertThat(attributesMap).containsEntry("foo", "bar"); + assertThat(attributesMap).containsEntry("baz", "qux"); + assertThat(attributesMap).doesNotContainEntry("foo", "blah"); + + Set> entrySet = attributesMap.entrySet(); + assertThat(entrySet).isNotEmpty(); + assertThat(entrySet).hasSize(attributesMap.size()); + assertThat(entrySet).contains(Map.entry("foo", "bar")); + assertThat(entrySet).contains(Map.entry("baz", "qux")); + assertThat(entrySet).doesNotContain(Map.entry("foo", "blah")); + assertThat(entrySet).isUnmodifiable(); + + assertThat(entrySet.iterator()).toIterable().contains(Map.entry("foo", "bar"), Map.entry("baz", "qux")); + Iterator attributes = servletRequest.getAttributeNames().asIterator(); + Iterator> entrySetIterator = entrySet.iterator(); + while (attributes.hasNext()) { + attributes.next(); + assertThat(entrySetIterator).hasNext(); + entrySetIterator.next(); + } + assertThat(entrySetIterator).isExhausted(); + + attributesMap.clear(); + assertThat(attributesMap).isEmpty(); + assertThat(attributesMap).hasSize(0); + assertThat(entrySet).isEmpty(); + assertThat(entrySet).hasSize(0); + assertThat(entrySet.iterator()).isExhausted(); + } + @Test void params() { MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true); From 5f13ea95fb8106b954bdec90816ddbb56e0d9984 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 5 Feb 2024 11:15:49 +0100 Subject: [PATCH 090/261] Polish See gh-32197 --- .../web/servlet/function/DefaultServerRequest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 4775d37f2bcb..3a4f4667d5a0 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 @@ -438,7 +438,8 @@ public boolean containsKey(Object key) { @Override public void clear() { - this.servletRequest.getAttributeNames().asIterator().forEachRemaining(this.servletRequest::removeAttribute); + List attributeNames = Collections.list(this.servletRequest.getAttributeNames()); + attributeNames.forEach(this.servletRequest::removeAttribute); } @Override From 5434edd726f1f4a6c330f2a4ebf2e00841af0ed7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 6 Feb 2024 16:46:04 +0100 Subject: [PATCH 091/261] Avoid sendError call when response committed already (Tomcat 10.1.16) Closes gh-32206 (cherry picked from commit 4ed337247ccd3e976cb544d831de023a1704c96d) --- .../DefaultHandlerExceptionResolver.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index 8dc9851bbe82..4067573c7e5a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -464,8 +464,8 @@ protected ModelAndView handleErrorResponse(ErrorResponse errorResponse, response.sendError(status); } } - else { - logger.warn("Ignoring exception, response committed. : " + errorResponse); + else if (logger.isWarnEnabled()) { + logger.warn("Ignoring exception, response committed already: " + errorResponse); } return new ModelAndView(); @@ -524,14 +524,19 @@ protected ModelAndView handleTypeMismatch(TypeMismatchException ex, protected ModelAndView handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); + if (!response.isCommitted()) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + } + else if (logger.isWarnEnabled()) { + logger.warn("Ignoring exception, response committed already: " + ex); + } return new ModelAndView(); } /** * Handle the case where a * {@linkplain org.springframework.http.converter.HttpMessageConverter message converter} - * cannot write to an HTTP request. + * cannot write to an HTTP response. *

    The default implementation sends an HTTP 500 error, and returns an empty {@code ModelAndView}. * Alternatively, a fallback view could be chosen, or the HttpMessageNotWritableException could * be rethrown as-is. @@ -545,7 +550,12 @@ protected ModelAndView handleHttpMessageNotReadable(HttpMessageNotReadableExcept protected ModelAndView handleHttpMessageNotWritable(HttpMessageNotWritableException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException { - sendServerError(ex, request, response); + if (!response.isCommitted()) { + sendServerError(ex, request, response); + } + else if (logger.isWarnEnabled()) { + logger.warn("Ignoring exception, response committed already: " + ex); + } return new ModelAndView(); } From 95a86463091b05aadc34e2d1b6fba8ff808c2656 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 6 Feb 2024 17:06:05 +0100 Subject: [PATCH 092/261] Polishing --- .../web/method/annotation/ModelFactory.java | 4 +-- .../method/annotation/ModelFactoryTests.java | 33 +++++++++---------- .../SessionAttributesHandlerTests.java | 30 +++++++++-------- .../DefaultHandlerExceptionResolver.java | 2 +- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java index 62af5c3e4a62..884f7caaf2a2 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -234,11 +234,9 @@ private boolean isBindingCandidate(String attributeName, Object value) { if (attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) { return false; } - if (this.sessionAttributesHandler.isHandlerSessionAttribute(attributeName, value.getClass())) { return true; } - return (!value.getClass().isArray() && !(value instanceof Collection) && !(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass())); } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java index c9091e108444..0433dac2efa4 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -51,7 +51,7 @@ * * @author Rossen Stoyanchev */ -public class ModelFactoryTests { +class ModelFactoryTests { private NativeWebRequest webRequest; @@ -65,7 +65,7 @@ public class ModelFactoryTests { @BeforeEach - public void setUp() throws Exception { + void setup() { this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); this.attributeStore = new DefaultSessionAttributeStore(); this.attributeHandler = new SessionAttributesHandler(TestController.class, this.attributeStore); @@ -75,7 +75,7 @@ public void setUp() throws Exception { @Test - public void modelAttributeMethod() throws Exception { + void modelAttributeMethod() throws Exception { ModelFactory modelFactory = createModelFactory("modelAttr", Model.class); HandlerMethod handlerMethod = createHandlerMethod("handle"); modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); @@ -84,7 +84,7 @@ public void modelAttributeMethod() throws Exception { } @Test - public void modelAttributeMethodWithExplicitName() throws Exception { + void modelAttributeMethodWithExplicitName() throws Exception { ModelFactory modelFactory = createModelFactory("modelAttrWithName"); HandlerMethod handlerMethod = createHandlerMethod("handle"); modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); @@ -93,7 +93,7 @@ public void modelAttributeMethodWithExplicitName() throws Exception { } @Test - public void modelAttributeMethodWithNameByConvention() throws Exception { + void modelAttributeMethodWithNameByConvention() throws Exception { ModelFactory modelFactory = createModelFactory("modelAttrConvention"); HandlerMethod handlerMethod = createHandlerMethod("handle"); modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); @@ -102,7 +102,7 @@ public void modelAttributeMethodWithNameByConvention() throws Exception { } @Test - public void modelAttributeMethodWithNullReturnValue() throws Exception { + void modelAttributeMethodWithNullReturnValue() throws Exception { ModelFactory modelFactory = createModelFactory("nullModelAttr"); HandlerMethod handlerMethod = createHandlerMethod("handle"); modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); @@ -112,7 +112,7 @@ public void modelAttributeMethodWithNullReturnValue() throws Exception { } @Test - public void modelAttributeWithBindingDisabled() throws Exception { + void modelAttributeWithBindingDisabled() throws Exception { ModelFactory modelFactory = createModelFactory("modelAttrWithBindingDisabled"); HandlerMethod handlerMethod = createHandlerMethod("handle"); modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); @@ -122,7 +122,7 @@ public void modelAttributeWithBindingDisabled() throws Exception { } @Test - public void modelAttributeFromSessionWithBindingDisabled() throws Exception { + void modelAttributeFromSessionWithBindingDisabled() throws Exception { Foo foo = new Foo(); this.attributeStore.storeAttribute(this.webRequest, "foo", foo); @@ -136,7 +136,7 @@ public void modelAttributeFromSessionWithBindingDisabled() throws Exception { } @Test - public void sessionAttribute() throws Exception { + void sessionAttribute() throws Exception { this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue"); ModelFactory modelFactory = createModelFactory("modelAttr", Model.class); @@ -147,7 +147,7 @@ public void sessionAttribute() throws Exception { } @Test - public void sessionAttributeNotPresent() throws Exception { + void sessionAttributeNotPresent() throws Exception { ModelFactory modelFactory = new ModelFactory(null, null, this.attributeHandler); HandlerMethod handlerMethod = createHandlerMethod("handleSessionAttr", String.class); assertThatExceptionOfType(HttpSessionRequiredException.class).isThrownBy(() -> @@ -155,13 +155,12 @@ public void sessionAttributeNotPresent() throws Exception { // Now add attribute and try again this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue"); - modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); assertThat(this.mavContainer.getModel().get("sessionAttr")).isEqualTo("sessionAttrValue"); } @Test - public void updateModelBindingResult() throws Exception { + void updateModelBindingResult() throws Exception { String commandName = "attr1"; Object command = new Object(); ModelAndViewContainer container = new ModelAndViewContainer(); @@ -181,7 +180,7 @@ public void updateModelBindingResult() throws Exception { } @Test - public void updateModelSessionAttributesSaved() throws Exception { + void updateModelSessionAttributesSaved() throws Exception { String attributeName = "sessionAttr"; String attribute = "value"; ModelAndViewContainer container = new ModelAndViewContainer(); @@ -199,7 +198,7 @@ public void updateModelSessionAttributesSaved() throws Exception { } @Test - public void updateModelSessionAttributesRemoved() throws Exception { + void updateModelSessionAttributesRemoved() throws Exception { String attributeName = "sessionAttr"; String attribute = "value"; ModelAndViewContainer container = new ModelAndViewContainer(); @@ -221,7 +220,7 @@ public void updateModelSessionAttributesRemoved() throws Exception { } @Test // SPR-12542 - public void updateModelWhenRedirecting() throws Exception { + void updateModelWhenRedirecting() throws Exception { String attributeName = "sessionAttr"; String attribute = "value"; ModelAndViewContainer container = new ModelAndViewContainer(); @@ -286,7 +285,7 @@ public Boolean nullModelAttr() { return null; } - @ModelAttribute(name="foo", binding=false) + @ModelAttribute(name = "foo", binding = false) public Foo modelAttrWithBindingDisabled() { return new Foo(); } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/SessionAttributesHandlerTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/SessionAttributesHandlerTests.java index 9c856d2d5337..782ec55c1e38 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/SessionAttributesHandlerTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/SessionAttributesHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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,6 @@ package org.springframework.web.method.annotation; - import java.util.HashSet; import org.junit.jupiter.api.Test; @@ -35,20 +34,21 @@ /** * Test fixture with {@link SessionAttributesHandler}. + * * @author Rossen Stoyanchev */ -public class SessionAttributesHandlerTests { +class SessionAttributesHandlerTests { private final SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore(); - private final SessionAttributesHandler sessionAttributesHandler = new SessionAttributesHandler( - SessionAttributeHandler.class, sessionAttributeStore); + private final SessionAttributesHandler sessionAttributesHandler = + new SessionAttributesHandler(TestSessionAttributesHolder.class, sessionAttributeStore); private final NativeWebRequest request = new ServletWebRequest(new MockHttpServletRequest()); @Test - public void isSessionAttribute() throws Exception { + void isSessionAttribute() { assertThat(sessionAttributesHandler.isHandlerSessionAttribute("attr1", String.class)).isTrue(); assertThat(sessionAttributesHandler.isHandlerSessionAttribute("attr2", String.class)).isTrue(); assertThat(sessionAttributesHandler.isHandlerSessionAttribute("simple", TestBean.class)).isTrue(); @@ -56,22 +56,26 @@ public void isSessionAttribute() throws Exception { } @Test - public void retrieveAttributes() throws Exception { + void retrieveAttributes() { sessionAttributeStore.storeAttribute(request, "attr1", "value1"); sessionAttributeStore.storeAttribute(request, "attr2", "value2"); sessionAttributeStore.storeAttribute(request, "attr3", new TestBean()); sessionAttributeStore.storeAttribute(request, "attr4", new TestBean()); - assertThat(sessionAttributesHandler.retrieveAttributes(request).keySet()).as("Named attributes (attr1, attr2) should be 'known' right away").isEqualTo(new HashSet<>(asList("attr1", "attr2"))); + assertThat(sessionAttributesHandler.retrieveAttributes(request).keySet()) + .as("Named attributes (attr1, attr2) should be 'known' right away") + .isEqualTo(new HashSet<>(asList("attr1", "attr2"))); // Resolve 'attr3' by type sessionAttributesHandler.isHandlerSessionAttribute("attr3", TestBean.class); - assertThat(sessionAttributesHandler.retrieveAttributes(request).keySet()).as("Named attributes (attr1, attr2) and resolved attribute (att3) should be 'known'").isEqualTo(new HashSet<>(asList("attr1", "attr2", "attr3"))); + assertThat(sessionAttributesHandler.retrieveAttributes(request).keySet()) + .as("Named attributes (attr1, attr2) and resolved attribute (attr3) should be 'known'") + .isEqualTo(new HashSet<>(asList("attr1", "attr2", "attr3"))); } @Test - public void cleanupAttributes() throws Exception { + void cleanupAttributes() { sessionAttributeStore.storeAttribute(request, "attr1", "value1"); sessionAttributeStore.storeAttribute(request, "attr2", "value2"); sessionAttributeStore.storeAttribute(request, "attr3", new TestBean()); @@ -90,7 +94,7 @@ public void cleanupAttributes() throws Exception { } @Test - public void storeAttributes() throws Exception { + void storeAttributes() { ModelMap model = new ModelMap(); model.put("attr1", "value1"); model.put("attr2", "value2"); @@ -105,8 +109,8 @@ public void storeAttributes() throws Exception { } - @SessionAttributes(names = { "attr1", "attr2" }, types = { TestBean.class }) - private static class SessionAttributeHandler { + @SessionAttributes(names = {"attr1", "attr2"}, types = TestBean.class) + private static class TestSessionAttributesHolder { } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index 4067573c7e5a..ce6d4aacdc52 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -438,7 +438,7 @@ protected ModelAndView handleAsyncRequestTimeoutException(AsyncRequestTimeoutExc * Handle an {@link ErrorResponse} exception. *

    The default implementation sets status and the headers of the response * to those obtained from the {@code ErrorResponse}. If available, the - * {@link ProblemDetail#getDetail()} is used as the message for + * {@link ProblemDetail#getDetail()} is used as the message for * {@link HttpServletResponse#sendError(int, String)}. * @param errorResponse the exception to be handled * @param request current HTTP request From 5fd9fab0ce07ef7596b3e7c5ba2f6f12adcf8a71 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 7 Feb 2024 23:40:11 +0100 Subject: [PATCH 093/261] Polishing (backported from main) --- .../language-ref/collection-projection.adoc | 12 ++++++---- .../language-ref/collection-selection.adoc | 23 ++++++++++--------- .../server-setup-options.adoc | 2 +- .../expression/TypedValue.java | 5 ++-- .../expression/spel/ast/Selection.java | 21 +++++++++-------- .../servlet/function/RequestPredicates.java | 12 ++++++++-- 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc index 83f492766dd0..efc43fe8c1ce 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-projection.adoc @@ -4,7 +4,7 @@ Projection lets a collection drive the evaluation of a sub-expression, and the result is a new collection. The syntax for projection is `.![projectionExpression]`. For example, suppose we have a list of inventors but want the list of cities where they were born. -Effectively, we want to evaluate 'placeOfBirth.city' for every entry in the inventor +Effectively, we want to evaluate `placeOfBirth.city` for every entry in the inventor list. The following example uses projection to do so: [tabs] @@ -13,16 +13,18 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - // returns ['Smiljan', 'Idvor' ] - List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]"); + // evaluates to ["SmilJan", "Idvor"] + List placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") + .getValue(societyContext, List.class); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - // returns ['Smiljan', 'Idvor' ] - val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> + // evaluates to ["SmilJan", "Idvor"] + val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") + .getValue(societyContext) as List<*> ---- ====== diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc index 3f87541a81cb..f0f70ffad9ac 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/collection-selection.adoc @@ -28,13 +28,14 @@ Kotlin:: ====== Selection is supported for arrays and anything that implements `java.lang.Iterable` or -`java.util.Map`. For a list or array, the selection criteria is evaluated against each -individual element. Against a map, the selection criteria is evaluated against each map -entry (objects of the Java type `Map.Entry`). Each map entry has its `key` and `value` -accessible as properties for use in the selection. +`java.util.Map`. For an array or `Iterable`, the selection expression is evaluated +against each individual element. Against a map, the selection expression is evaluated +against each map entry (objects of the Java type `Map.Entry`). Each map entry has its +`key` and `value` accessible as properties for use in the selection. -The following expression returns a new map that consists of those elements of the -original map where the entry's value is less than 27: +Given a `Map` stored in a variable named `#map`, the following expression returns a new +map that consists of those elements of the original map where the entry's value is less +than 27: [tabs] ====== @@ -42,21 +43,21 @@ Java:: + [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- - Map newMap = parser.parseExpression("map.?[value<27]").getValue(); + Map newMap = parser.parseExpression("#map.?[value < 27]").getValue(Map.class); ---- Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] ---- - val newMap = parser.parseExpression("map.?[value<27]").getValue() + val newMap = parser.parseExpression("#map.?[value < 27]").getValue() as Map ---- ====== In addition to returning all the selected elements, you can retrieve only the first or -the last element. To obtain the first element matching the selection, the syntax is -`.^[selectionExpression]`. To obtain the last matching selection, the syntax is -`.$[selectionExpression]`. +the last element. To obtain the first element matching the selection expression, the +syntax is `.^[selectionExpression]`. To obtain the last element matching the selection +expression, the syntax is `.$[selectionExpression]`. diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-options.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-options.adoc index b38d69ed683e..9dd0498d4e94 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-options.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/server-setup-options.adoc @@ -152,7 +152,7 @@ Kotlin:: @Autowired lateinit var accountService: AccountService - lateinit mockMvc: MockMvc + lateinit var mockMvc: MockMvc @BeforeEach fun setup(wac: WebApplicationContext) { diff --git a/spring-expression/src/main/java/org/springframework/expression/TypedValue.java b/spring-expression/src/main/java/org/springframework/expression/TypedValue.java index ccd8b5723524..b97020a259ec 100644 --- a/spring-expression/src/main/java/org/springframework/expression/TypedValue.java +++ b/spring-expression/src/main/java/org/springframework/expression/TypedValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -22,7 +22,8 @@ /** * Encapsulates an object and a {@link TypeDescriptor} that describes it. - * The type descriptor can contain generic declarations that would not + * + *

    The type descriptor can contain generic declarations that would not * be accessible through a simple {@code getClass()} call on the object. * * @author Andy Clement diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index 72f9aa6f0e88..374a9da4d935 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,7 +36,9 @@ /** * Represents selection over a map or collection. - * For example: {1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this) == 'y'} returns [2, 4, 6, 8, 10] + * + *

    For example, {1,2,3,4,5,6,7,8,9,10}.?{#isEven(#this)} evaluates + * to {@code [2, 4, 6, 8, 10]}. * *

    Basically a subset of the input data is returned based on the * evaluation of the expression supplied as selection criteria. @@ -100,11 +102,10 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException Object val = selectionCriteria.getValueInternal(state).getValue(); if (val instanceof Boolean b) { if (b) { + result.put(entry.getKey(), entry.getValue()); if (this.variant == FIRST) { - result.put(entry.getKey(), entry.getValue()); return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); } - result.put(entry.getKey(), entry.getValue()); lastKey = entry.getKey(); } } @@ -120,22 +121,22 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException } if ((this.variant == FIRST || this.variant == LAST) && result.isEmpty()) { - return new ValueRef.TypedValueHolderValueRef(new TypedValue(null), this); + return new ValueRef.TypedValueHolderValueRef(TypedValue.NULL, this); } if (this.variant == LAST) { Map resultMap = new HashMap<>(); Object lastValue = result.get(lastKey); - resultMap.put(lastKey,lastValue); - return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultMap),this); + resultMap.put(lastKey, lastValue); + return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultMap), this); } - return new ValueRef.TypedValueHolderValueRef(new TypedValue(result),this); + return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); } if (operand instanceof Iterable || ObjectUtils.isArray(operand)) { - Iterable data = (operand instanceof Iterable iterable ? - iterable : Arrays.asList(ObjectUtils.toObjectArray(operand))); + Iterable data = (operand instanceof Iterable iterable ? iterable : + Arrays.asList(ObjectUtils.toObjectArray(operand))); List result = new ArrayList<>(); int index = 0; 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 b71277ed16db..9923bfd7cfb5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +106,7 @@ public static RequestPredicate methods(HttpMethod... httpMethods) { * against the given path pattern. * @param pattern the pattern to match to * @return a predicate that tests against the given path pattern + * @see org.springframework.web.util.pattern.PathPattern */ public static RequestPredicate path(String pattern) { Assert.notNull(pattern, "'pattern' must not be null"); @@ -168,6 +169,7 @@ public static RequestPredicate accept(MediaType... mediaTypes) { * @param pattern the path pattern to match against * @return a predicate that matches if the request method is GET and if the given pattern * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern */ public static RequestPredicate GET(String pattern) { return method(HttpMethod.GET).and(path(pattern)); @@ -179,6 +181,7 @@ public static RequestPredicate GET(String pattern) { * @param pattern the path pattern to match against * @return a predicate that matches if the request method is HEAD and if the given pattern * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern */ public static RequestPredicate HEAD(String pattern) { return method(HttpMethod.HEAD).and(path(pattern)); @@ -190,6 +193,7 @@ public static RequestPredicate HEAD(String pattern) { * @param pattern the path pattern to match against * @return a predicate that matches if the request method is POST and if the given pattern * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern */ public static RequestPredicate POST(String pattern) { return method(HttpMethod.POST).and(path(pattern)); @@ -201,6 +205,7 @@ public static RequestPredicate POST(String pattern) { * @param pattern the path pattern to match against * @return a predicate that matches if the request method is PUT and if the given pattern * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern */ public static RequestPredicate PUT(String pattern) { return method(HttpMethod.PUT).and(path(pattern)); @@ -212,6 +217,7 @@ public static RequestPredicate PUT(String pattern) { * @param pattern the path pattern to match against * @return a predicate that matches if the request method is PATCH and if the given pattern * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern */ public static RequestPredicate PATCH(String pattern) { return method(HttpMethod.PATCH).and(path(pattern)); @@ -223,6 +229,7 @@ public static RequestPredicate PATCH(String pattern) { * @param pattern the path pattern to match against * @return a predicate that matches if the request method is DELETE and if the given pattern * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern */ public static RequestPredicate DELETE(String pattern) { return method(HttpMethod.DELETE).and(path(pattern)); @@ -234,6 +241,7 @@ public static RequestPredicate DELETE(String pattern) { * @param pattern the path pattern to match against * @return a predicate that matches if the request method is OPTIONS and if the given pattern * matches against the request path + * @see org.springframework.web.util.pattern.PathPattern */ public static RequestPredicate OPTIONS(String pattern) { return method(HttpMethod.OPTIONS).and(path(pattern)); @@ -317,7 +325,6 @@ private static PathPattern mergePatterns(@Nullable PathPattern oldPattern, PathP else { return newPattern; } - } @@ -337,6 +344,7 @@ public interface Visitor { * Receive notification of a path predicate. * @param pattern the path pattern that makes up the predicate * @see RequestPredicates#path(String) + * @see org.springframework.web.util.pattern.PathPattern */ void path(String pattern); From 667e4e753188ec13a8640b3b98d40edd1f991100 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 8 Feb 2024 12:22:14 +0100 Subject: [PATCH 094/261] Implement MatchableHandlerMapping in RouterFunctionMapping See gh-32221 Closes gh-32222 --- .../function/support/RouterFunctionMapping.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 f67ea7e2e692..600d1db2918f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,6 +37,8 @@ import org.springframework.web.servlet.function.RouterFunctions; import org.springframework.web.servlet.function.ServerRequest; import org.springframework.web.servlet.handler.AbstractHandlerMapping; +import org.springframework.web.servlet.handler.MatchableHandlerMapping; +import org.springframework.web.servlet.handler.RequestMatchResult; import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; @@ -53,7 +55,7 @@ * @author Brian Clozel * @since 5.2 */ -public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean { +public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean, MatchableHandlerMapping { @Nullable private RouterFunction routerFunction; @@ -223,4 +225,9 @@ private void setAttributes(HttpServletRequest servletRequest, ServerRequest requ servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, request); } + @Nullable + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + throw new UnsupportedOperationException("This HandlerMapping uses PathPatterns"); + } } From b11ff966521cd1b4038b3b0d876ff0956e7ca8de Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 9 Feb 2024 11:08:44 +0000 Subject: [PATCH 095/261] Update user info pattern Closes gh-32211 --- .../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 33cdc834166a..afea8c3a5397 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -77,7 +77,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HTTP_PATTERN = "(?i)(http|https):"; - private static final String USERINFO_PATTERN = "([^@\\[/?#]*)"; + private static final String USERINFO_PATTERN = "([^@/?#]*)"; private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*"; From b976ee3f67f66adf2de7c292cb3e8ffb04a969b4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 13 Feb 2024 11:07:20 +0100 Subject: [PATCH 096/261] Consistent Lock field declaration (instead of ReentrantLock field type) (cherry picked from commit b4153618a414f43d12feb2b33487fa1a35be4674) --- .../support/ReloadableResourceBundleMessageSource.java | 7 ++++--- .../web/server/session/InMemoryWebSessionStore.java | 7 +++---- .../web/socket/messaging/SubProtocolWebSocketHandler.java | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index 6731999c0b83..d36163078d1a 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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 java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.springframework.context.ResourceLoaderAware; @@ -400,7 +401,7 @@ protected PropertiesHolder getProperties(String filename) { /** * Refresh the PropertiesHolder for the given bundle filename. - * The holder can be {@code null} if not cached before, or a timed-out cache entry + *

    The holder can be {@code null} if not cached before, or a timed-out cache entry * (potentially getting re-validated against the current last-modified timestamp). * @param filename the bundle filename (basename + Locale) * @param propHolder the current PropertiesHolder for the bundle @@ -561,7 +562,7 @@ protected class PropertiesHolder { private volatile long refreshTimestamp = -2; - private final ReentrantLock refreshLock = new ReentrantLock(); + private final Lock refreshLock = new ReentrantLock(); /** Cache to hold already generated MessageFormats per message code. */ private final ConcurrentMap> cachedMessageFormats = diff --git a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java index 504ee546d32a..ecc1557d6a82 100644 --- a/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java +++ b/spring-web/src/main/java/org/springframework/web/server/session/InMemoryWebSessionStore.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -26,6 +26,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import reactor.core.publisher.Mono; @@ -315,12 +316,10 @@ private class ExpiredSessionChecker { /** Max time between expiration checks. */ private static final int CHECK_PERIOD = 60 * 1000; - - private final ReentrantLock lock = new ReentrantLock(); + private final Lock lock = new ReentrantLock(); private Instant checkTime = clock.instant().plus(CHECK_PERIOD, ChronoUnit.MILLIS); - public void checkIfNecessary(Instant now) { if (this.checkTime.isBefore(now)) { removeExpiredSessions(now); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java index 3d8868e95908..4e1cd54e01db 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/messaging/SubProtocolWebSocketHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; @@ -98,7 +99,7 @@ public class SubProtocolWebSocketHandler private volatile long lastSessionCheckTime = System.currentTimeMillis(); - private final ReentrantLock sessionCheckLock = new ReentrantLock(); + private final Lock sessionCheckLock = new ReentrantLock(); private final DefaultStats stats = new DefaultStats(); From 70be04ba814e187296f21fdb43a01919d24720dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 13 Feb 2024 15:13:53 +0100 Subject: [PATCH 097/261] Infer reflection hints for Jackson annotations `builder` attributes This notably enables Jackson to reflectively call a user-provided builder class and invoke its declared methods (setters and build) in a native app. See gh-32238 Closes gh-32257 --- .../aot/hint/BindingReflectionHintsRegistrar.java | 10 ++++++++-- .../aot/hint/BindingReflectionHintsRegistrarTests.java | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java index ac0375f2dd13..9e5173ab7e66 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java @@ -195,9 +195,15 @@ private void forEachJacksonAnnotation(AnnotatedElement element, Consumer annotation) { - annotation.getRoot().asMap().values().forEach(value -> { + annotation.getRoot().asMap().forEach((key,value) -> { if (value instanceof Class classValue && value != Void.class) { - hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + if (key.equals("builder")) { + hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS); + } + else { + hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + } } }); } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java index d057197d9ff8..ab1d86c03c82 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java @@ -289,7 +289,8 @@ void registerTypeForJacksonCustomStrategy() { bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleRecordWithJacksonCustomStrategy.class); assertThat(RuntimeHintsPredicates.reflection().onType(PropertyNamingStrategies.UpperSnakeCaseStrategy.class).withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) .accepts(this.hints); - assertThat(RuntimeHintsPredicates.reflection().onType(SampleRecordWithJacksonCustomStrategy.Builder.class).withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) + assertThat(RuntimeHintsPredicates.reflection().onType(SampleRecordWithJacksonCustomStrategy.Builder.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS)) .accepts(this.hints); } From aaf90f5a96ae570fc0ea62dd66509561952ba8db Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 15 Feb 2024 10:28:18 +0100 Subject: [PATCH 098/261] Upgrade to Reactor 2022.0.16 Includes SLF4J 2.0.12, Groovy 4.0.18, Jetty 11.0.20, Netty 4.1.107, OpenPDF 1.3.40 --- framework-platform/framework-platform.gradle | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 3315655d6dbd..ecc1cd3952df 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,13 +9,13 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.14.3")) api(platform("io.micrometer:micrometer-bom:1.10.13")) - api(platform("io.netty:netty-bom:4.1.104.Final")) + api(platform("io.netty:netty-bom:4.1.107.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.15")) + api(platform("io.projectreactor:reactor-bom:2022.0.16")) api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.17")) + api(platform("org.apache.groovy:groovy-bom:4.0.18")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) - api(platform("org.eclipse.jetty:jetty-bom:11.0.19")) + api(platform("org.eclipse.jetty:jetty-bom:11.0.20")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) api(platform("org.junit:junit-bom:5.9.3")) @@ -25,7 +25,7 @@ dependencies { api("com.fasterxml:aalto-xml:1.3.2") api("com.fasterxml.woodstox:woodstox-core:6.5.1") api("com.github.ben-manes.caffeine:caffeine:3.1.8") - api("com.github.librepdf:openpdf:1.3.36") + api("com.github.librepdf:openpdf:1.3.40") api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:jsr305:3.0.2") api("com.google.code.gson:gson:2.10.1") @@ -111,7 +111,7 @@ dependencies { api("org.bouncycastle:bcpkix-jdk18on:1.72") api("org.codehaus.jettison:jettison:1.5.4") api("org.dom4j:dom4j:2.1.4") - api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.10") + api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.12") api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4") api("org.eclipse:yasson:2.0.4") api("org.ehcache:ehcache:3.10.8") @@ -135,7 +135,7 @@ dependencies { api("org.seleniumhq.selenium:htmlunit-driver:2.70.0") api("org.seleniumhq.selenium:selenium-java:3.141.59") api("org.skyscreamer:jsonassert:1.5.1") - api("org.slf4j:slf4j-api:2.0.11") + api("org.slf4j:slf4j-api:2.0.12") api("org.testng:testng:7.8.0") api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-core:0.55") From ee100cfba7e583065fbdbd6f9b5b0c13f4ee0833 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 15 Feb 2024 10:49:54 +0100 Subject: [PATCH 099/261] Upgrade to Mockito 5.10, Mutiny 1.10, Undertow 2.3.11 --- framework-platform/framework-platform.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index ecc1cd3952df..771346b10e53 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -19,7 +19,7 @@ dependencies { api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) api(platform("org.junit:junit-bom:5.9.3")) - api(platform("org.mockito:mockito-bom:5.8.0")) + api(platform("org.mockito:mockito-bom:5.10.0")) constraints { api("com.fasterxml:aalto-xml:1.3.2") @@ -53,10 +53,10 @@ dependencies { api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE") api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") api("io.reactivex.rxjava3:rxjava:3.1.8") - api("io.smallrye.reactive:mutiny:1.9.0") - api("io.undertow:undertow-core:2.3.10.Final") - api("io.undertow:undertow-servlet:2.3.10.Final") - api("io.undertow:undertow-websockets-jsr:2.3.10.Final") + api("io.smallrye.reactive:mutiny:1.10.0") + api("io.undertow:undertow-core:2.3.11.Final") + api("io.undertow:undertow-servlet:2.3.11.Final") + api("io.undertow:undertow-websockets-jsr:2.3.11.Final") api("io.vavr:vavr:0.10.4") api("jakarta.activation:jakarta.activation-api:2.0.1") api("jakarta.annotation:jakarta.annotation-api:2.0.0") From 8f286de773c657103189ffedded3c40bb5d00eb3 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 15 Feb 2024 12:34:07 +0000 Subject: [PATCH 100/261] Next development version (v6.0.18-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0d3033eb9645..27eecbd21650 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.17-SNAPSHOT +version=6.0.18-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 8fba4a448a217f5a749fe01d46816c05f1c140e4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 16 Feb 2024 22:27:09 +0100 Subject: [PATCH 101/261] Polishing --- .../ConfigurationClassPostProcessorTests.java | 119 ++++++++---------- .../ConfigurationClassProcessingTests.java | 26 ++-- .../core/annotation/OrderUtils.java | 8 +- .../DefaultMessageListenerContainer.java | 6 +- .../jms/annotation/EnableJmsTests.java | 41 +++--- .../JmsListenerContainerTestFactory.java | 6 +- .../connection/SingleConnectionFactory.java | 8 +- 7 files changed, 99 insertions(+), 115 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 c21783c9c817..2c3a188ce20a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -321,8 +321,8 @@ private void assertSupportForComposedAnnotationWithExclude(RootBeanDefinition be ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.setEnvironment(new StandardEnvironment()); pp.postProcessBeanFactory(beanFactory); - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - beanFactory.getBean(SimpleComponent.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> beanFactory.getBean(SimpleComponent.class)); } @Test @@ -372,11 +372,11 @@ void postProcessorFailsOnImplicitOverrideIfOverridingIsNotAllowed() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(SingletonBeanConfig.class)); beanFactory.setAllowBeanDefinitionOverriding(false); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> - pp.postProcessBeanFactory(beanFactory)) - .withMessageContaining("bar") - .withMessageContaining("SingletonBeanConfig") - .withMessageContaining(TestBean.class.getName()); + assertThatExceptionOfType(BeanDefinitionStoreException.class) + .isThrownBy(() -> pp.postProcessBeanFactory(beanFactory)) + .withMessageContaining("bar") + .withMessageContaining("SingletonBeanConfig") + .withMessageContaining(TestBean.class.getName()); } @Test // gh-25430 @@ -429,12 +429,12 @@ void configurationClassesWithInvalidOverridingForProgrammaticCall() { ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); pp.postProcessBeanFactory(beanFactory); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - beanFactory.getBean(Bar.class)) - .withMessageContaining("OverridingSingletonBeanConfig.foo") - .withMessageContaining(ExtendedFoo.class.getName()) - .withMessageContaining(Foo.class.getName()) - .withMessageContaining("InvalidOverridingSingletonBeanConfig"); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> beanFactory.getBean(Bar.class)) + .withMessageContaining("OverridingSingletonBeanConfig.foo") + .withMessageContaining(ExtendedFoo.class.getName()) + .withMessageContaining(Foo.class.getName()) + .withMessageContaining("InvalidOverridingSingletonBeanConfig"); } @Test // SPR-15384 @@ -985,16 +985,16 @@ void testCircularDependency() { beanFactory.registerBeanDefinition("configClass1", new RootBeanDefinition(A.class)); beanFactory.registerBeanDefinition("configClass2", new RootBeanDefinition(AStrich.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - beanFactory::preInstantiateSingletons) - .withMessageContaining("Circular reference"); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(beanFactory::preInstantiateSingletons) + .withMessageContaining("Circular reference"); } @Test void testCircularDependencyWithApplicationContext() { - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - new AnnotationConfigApplicationContext(A.class, AStrich.class)) - .withMessageContaining("Circular reference"); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(A.class, AStrich.class)) + .withMessageContaining("Circular reference"); } @Test @@ -1048,9 +1048,7 @@ void testEmptyVarargOnBeanMethod() { void testCollectionArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class, TestBean.class); CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans).hasSize(1); - assertThat(bean.testBeans.get(0)).isSameAs(ctx.getBean(TestBean.class)); + assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class)); ctx.close(); } @@ -1058,8 +1056,7 @@ void testCollectionArgumentOnBeanMethod() { void testEmptyCollectionArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionArgumentConfiguration.class); CollectionArgumentConfiguration bean = ctx.getBean(CollectionArgumentConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans.isEmpty()).isTrue(); + assertThat(bean.testBeans).isEmpty(); ctx.close(); } @@ -1067,9 +1064,7 @@ void testEmptyCollectionArgumentOnBeanMethod() { void testMapArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class, DummyRunnable.class); MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans).hasSize(1); - assertThat(bean.testBeans.values().iterator().next()).isSameAs(ctx.getBean(Runnable.class)); + assertThat(bean.testBeans).hasSize(1).containsValue(ctx.getBean(Runnable.class)); ctx.close(); } @@ -1077,8 +1072,7 @@ void testMapArgumentOnBeanMethod() { void testEmptyMapArgumentOnBeanMethod() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapArgumentConfiguration.class); MapArgumentConfiguration bean = ctx.getBean(MapArgumentConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans.isEmpty()).isTrue(); + assertThat(bean.testBeans).isEmpty(); ctx.close(); } @@ -1086,9 +1080,7 @@ void testEmptyMapArgumentOnBeanMethod() { void testCollectionInjectionFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CollectionInjectionConfiguration.class); CollectionInjectionConfiguration bean = ctx.getBean(CollectionInjectionConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans).hasSize(1); - assertThat(bean.testBeans.get(0)).isSameAs(ctx.getBean(TestBean.class)); + assertThat(bean.testBeans).containsExactly(ctx.getBean(TestBean.class)); ctx.close(); } @@ -1096,25 +1088,21 @@ void testCollectionInjectionFromSameConfigurationClass() { void testMapInjectionFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MapInjectionConfiguration.class); MapInjectionConfiguration bean = ctx.getBean(MapInjectionConfiguration.class); - assertThat(bean.testBeans).isNotNull(); - assertThat(bean.testBeans).hasSize(1); - assertThat(bean.testBeans.get("testBean")).isSameAs(ctx.getBean(Runnable.class)); + assertThat(bean.testBeans).containsOnly(Map.entry("testBean", ctx.getBean(Runnable.class))); ctx.close(); } @Test void testBeanLookupFromSameConfigurationClass() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(BeanLookupConfiguration.class); - BeanLookupConfiguration bean = ctx.getBean(BeanLookupConfiguration.class); - assertThat(bean.getTestBean()).isNotNull(); - assertThat(bean.getTestBean()).isSameAs(ctx.getBean(TestBean.class)); + assertThat(ctx.getBean(BeanLookupConfiguration.class).getTestBean()).isSameAs(ctx.getBean(TestBean.class)); ctx.close(); } @Test void testNameClashBetweenConfigurationClassAndBean() { assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class)); + .isThrownBy(() -> new AnnotationConfigApplicationContext(MyTestBean.class).getBean("myTestBean", TestBean.class)); } @Test @@ -1131,11 +1119,11 @@ void testBeanDefinitionRegistryPostProcessorConfig() { @Order(1) static class SingletonBeanConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1143,11 +1131,11 @@ static class SingletonBeanConfig { @Configuration(proxyBeanMethods = false) static class NonEnhancedSingletonBeanConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1155,11 +1143,13 @@ static class NonEnhancedSingletonBeanConfig { @Configuration static class StaticSingletonBeanConfig { - public static @Bean Foo foo() { + @Bean + public static Foo foo() { return new Foo(); } - public static @Bean Bar bar() { + @Bean + public static Bar bar() { return new Bar(foo()); } } @@ -1168,11 +1158,11 @@ static class StaticSingletonBeanConfig { @Order(2) static class OverridingSingletonBeanConfig { - public @Bean ExtendedFoo foo() { + @Bean public ExtendedFoo foo() { return new ExtendedFoo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1180,7 +1170,7 @@ static class OverridingSingletonBeanConfig { @Configuration static class OverridingAgainSingletonBeanConfig { - public @Bean ExtendedAgainFoo foo() { + @Bean public ExtendedAgainFoo foo() { return new ExtendedAgainFoo(); } } @@ -1188,7 +1178,7 @@ static class OverridingAgainSingletonBeanConfig { @Configuration static class InvalidOverridingSingletonBeanConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } } @@ -1200,11 +1190,11 @@ static class ConfigWithOrderedNestedClasses { @Order(1) static class SingletonBeanConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1213,11 +1203,11 @@ static class SingletonBeanConfig { @Order(2) static class OverridingSingletonBeanConfig { - public @Bean ExtendedFoo foo() { + @Bean public ExtendedFoo foo() { return new ExtendedFoo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1233,11 +1223,11 @@ class SingletonBeanConfig { public SingletonBeanConfig(ConfigWithOrderedInnerClasses other) { } - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1250,11 +1240,11 @@ public OverridingSingletonBeanConfig(ObjectProvider other) other.getObject(); } - public @Bean ExtendedFoo foo() { + @Bean public ExtendedFoo foo() { return new ExtendedFoo(); } - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(foo()); } } @@ -1281,7 +1271,7 @@ public Bar(Foo foo) { @Configuration static class UnloadedConfig { - public @Bean Foo foo() { + @Bean public Foo foo() { return new Foo(); } } @@ -1289,7 +1279,7 @@ static class UnloadedConfig { @Configuration static class LoadedConfig { - public @Bean Bar bar() { + @Bean public Bar bar() { return new Bar(new Foo()); } } @@ -1598,7 +1588,7 @@ public Object repoConsumer(Repository repo) { public static class WildcardWithGenericExtendsConfiguration { @Bean - public Repository genericRepo() { + public Repository genericRepo() { return new Repository(); } @@ -1707,7 +1697,7 @@ public String getParameter() { } @Configuration - public static abstract class AbstractConfig { + public abstract static class AbstractConfig { @Bean public ServiceBean serviceBean() { @@ -1758,7 +1748,6 @@ default ServiceBean serviceBean() { } public interface DefaultMethodsConfig extends BaseDefaultMethods { - } @Configuration @@ -1891,7 +1880,7 @@ static class DependingFoo { } } - static abstract class FooFactory { + abstract static class FooFactory { abstract DependingFoo createFoo(BarArgument bar); } @@ -2010,7 +1999,7 @@ private boolean testBean(boolean param) { } @Configuration - static abstract class BeanLookupConfiguration { + abstract static class BeanLookupConfiguration { @Bean public TestBean thing() { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java index 9c715f1d25f3..6375ed3a698b 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationClassProcessingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -204,7 +204,7 @@ void configurationWithNullReference() { BeanFactory factory = initBeanFactory(ConfigWithNullReference.class); TestBean foo = factory.getBean("foo", TestBean.class); - assertThat(factory.getBean("bar").equals(null)).isTrue(); + assertThat(factory.getBean("bar")).isEqualTo(null); assertThat(foo.getSpouse()).isNull(); } @@ -426,7 +426,7 @@ public Set get() { @Configuration static class ConfigWithFinalBean { - public final @Bean TestBean testBean() { + @Bean public final TestBean testBean() { return new TestBean(); } } @@ -435,7 +435,7 @@ static class ConfigWithFinalBean { @Configuration static class SimplestPossibleConfig { - public @Bean String stringBean() { + @Bean public String stringBean() { return "foo"; } } @@ -444,11 +444,11 @@ static class SimplestPossibleConfig { @Configuration static class ConfigWithNonSpecificReturnTypes { - public @Bean Object stringBean() { + @Bean public Object stringBean() { return "foo"; } - public @Bean FactoryBean factoryBean() { + @Bean public FactoryBean factoryBean() { ListFactoryBean fb = new ListFactoryBean(); fb.setSourceList(Arrays.asList("element1", "element2")); return fb; @@ -459,13 +459,13 @@ static class ConfigWithNonSpecificReturnTypes { @Configuration static class ConfigWithPrototypeBean { - public @Bean TestBean foo() { + @Bean public TestBean foo() { TestBean foo = new SpousyTestBean("foo"); foo.setSpouse(bar()); return foo; } - public @Bean TestBean bar() { + @Bean public TestBean bar() { TestBean bar = new SpousyTestBean("bar"); bar.setSpouse(baz()); return bar; @@ -605,15 +605,15 @@ static class ConfigWithFunctionalRegistration { void register(GenericApplicationContext ctx) { ctx.registerBean("spouse", TestBean.class, () -> new TestBean("functional")); - Supplier testBeanSupplier = () -> new TestBean(ctx.getBean("spouse", TestBean.class)); - ctx.registerBean(TestBean.class, - testBeanSupplier, + Supplier testBeanSupplier = + () -> new TestBean(ctx.getBean("spouse", TestBean.class)); + ctx.registerBean(TestBean.class, testBeanSupplier, bd -> bd.setPrimary(true)); } @Bean - public NestedTestBean nestedTestBean(TestBean testBean) { - return new NestedTestBean(testBean.getSpouse().getName()); + public NestedTestBean nestedTestBean(TestBean spouse) { + return new NestedTestBean(spouse.getSpouse().getName()); } } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java index 19fbc577b885..a18253f48cae 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,7 +38,7 @@ public abstract class OrderUtils { /** Cache marker for a non-annotated Class. */ private static final Object NOT_ANNOTATED = new Object(); - private static final String JAVAX_PRIORITY_ANNOTATION = "jakarta.annotation.Priority"; + private static final String JAKARTA_PRIORITY_ANNOTATION = "jakarta.annotation.Priority"; /** Cache for @Order value (or NOT_ANNOTATED marker) per Class. */ static final Map orderCache = new ConcurrentReferenceHashMap<>(64); @@ -124,7 +124,7 @@ private static Integer findOrder(MergedAnnotations annotations) { if (orderAnnotation.isPresent()) { return orderAnnotation.getInt(MergedAnnotation.VALUE); } - MergedAnnotation priorityAnnotation = annotations.get(JAVAX_PRIORITY_ANNOTATION); + MergedAnnotation priorityAnnotation = annotations.get(JAKARTA_PRIORITY_ANNOTATION); if (priorityAnnotation.isPresent()) { return priorityAnnotation.getInt(MergedAnnotation.VALUE); } @@ -139,7 +139,7 @@ private static Integer findOrder(MergedAnnotations annotations) { */ @Nullable public static Integer getPriority(Class type) { - return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).get(JAVAX_PRIORITY_ANNOTATION) + return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).get(JAKARTA_PRIORITY_ANNOTATION) .getValue(MergedAnnotation.VALUE, Integer.class).orElse(null); } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java index af2a2993db3d..fc48d3d495f4 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -284,6 +284,7 @@ public void setCacheLevelName(String constantName) throws IllegalArgumentExcepti * @see #CACHE_CONNECTION * @see #CACHE_SESSION * @see #CACHE_CONSUMER + * @see #CACHE_AUTO * @see #setCacheLevelName * @see #setTransactionManager */ @@ -570,8 +571,7 @@ public void initialize() { if (this.taskExecutor == null) { this.taskExecutor = createDefaultTaskExecutor(); } - else if (this.taskExecutor instanceof SchedulingTaskExecutor ste && - ste.prefersShortLivedTasks() && + else if (this.taskExecutor instanceof SchedulingTaskExecutor ste && ste.prefersShortLivedTasks() && this.maxMessagesPerTask == Integer.MIN_VALUE) { // TaskExecutor indicated a preference for short-lived tasks. According to // setMaxMessagesPerTask javadoc, we'll use 10 message per task in this case diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java index bb7be165b816..828050766776 100644 --- a/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,7 +19,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import jakarta.jms.JMSException; import jakarta.jms.MessageListener; import org.junit.jupiter.api.Test; @@ -103,24 +102,20 @@ void defaultContainerFactory() { } @Test - @SuppressWarnings("resource") void containerAreStartedByDefault() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( EnableJmsDefaultContainerFactoryConfig.class, DefaultBean.class); - JmsListenerContainerTestFactory factory = - context.getBean(JmsListenerContainerTestFactory.class); + JmsListenerContainerTestFactory factory = context.getBean(JmsListenerContainerTestFactory.class); MessageListenerTestContainer container = factory.getListenerContainers().get(0); assertThat(container.isAutoStartup()).isTrue(); assertThat(container.isStarted()).isTrue(); } @Test - @SuppressWarnings("resource") void containerCanBeStarterViaTheRegistry() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( EnableJmsAutoStartupFalseConfig.class, DefaultBean.class); - JmsListenerContainerTestFactory factory = - context.getBean(JmsListenerContainerTestFactory.class); + JmsListenerContainerTestFactory factory = context.getBean(JmsListenerContainerTestFactory.class); MessageListenerTestContainer container = factory.getListenerContainers().get(0); assertThat(container.isAutoStartup()).isFalse(); assertThat(container.isStarted()).isFalse(); @@ -131,13 +126,13 @@ void containerCanBeStarterViaTheRegistry() { @Override @Test - void jmsHandlerMethodFactoryConfiguration() throws JMSException { + void jmsHandlerMethodFactoryConfiguration() { ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( EnableJmsHandlerMethodFactoryConfig.class, ValidationBean.class); - assertThatExceptionOfType(ListenerExecutionFailedException.class).isThrownBy(() -> - testJmsHandlerMethodFactoryConfiguration(context)) - .withCauseInstanceOf(MethodArgumentNotValidException.class); + assertThatExceptionOfType(ListenerExecutionFailedException.class) + .isThrownBy(() -> testJmsHandlerMethodFactoryConfiguration(context)) + .withCauseInstanceOf(MethodArgumentNotValidException.class); } @Override @@ -159,19 +154,20 @@ void jmsListeners() { @Test void composedJmsListeners() { try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( - EnableJmsDefaultContainerFactoryConfig.class, ComposedJmsListenersBean.class)) { - JmsListenerContainerTestFactory simpleFactory = context.getBean("jmsListenerContainerFactory", - JmsListenerContainerTestFactory.class); + EnableJmsDefaultContainerFactoryConfig.class, ComposedJmsListenersBean.class)) { + + JmsListenerContainerTestFactory simpleFactory = + context.getBean("jmsListenerContainerFactory", JmsListenerContainerTestFactory.class); assertThat(simpleFactory.getListenerContainers()).hasSize(2); - MethodJmsListenerEndpoint first = (MethodJmsListenerEndpoint) simpleFactory.getListenerContainer( - "first").getEndpoint(); + MethodJmsListenerEndpoint first = (MethodJmsListenerEndpoint) + simpleFactory.getListenerContainer("first").getEndpoint(); assertThat(first.getId()).isEqualTo("first"); assertThat(first.getDestination()).isEqualTo("orderQueue"); assertThat(first.getConcurrency()).isNull(); - MethodJmsListenerEndpoint second = (MethodJmsListenerEndpoint) simpleFactory.getListenerContainer( - "second").getEndpoint(); + MethodJmsListenerEndpoint second = (MethodJmsListenerEndpoint) + simpleFactory.getListenerContainer("second").getEndpoint(); assertThat(second.getId()).isEqualTo("second"); assertThat(second.getDestination()).isEqualTo("billingQueue"); assertThat(second.getConcurrency()).isEqualTo("2-10"); @@ -179,12 +175,11 @@ void composedJmsListeners() { } @Test - @SuppressWarnings("resource") void unknownFactory() { // not found - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - new AnnotationConfigApplicationContext(EnableJmsSampleConfig.class, CustomBean.class)) - .withMessageContaining("customFactory"); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> new AnnotationConfigApplicationContext(EnableJmsSampleConfig.class, CustomBean.class)) + .withMessageContaining("customFactory"); } @Test diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java index 3eeb7db1141b..0c055f6fabb8 100644 --- a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -28,15 +28,13 @@ public class JmsListenerContainerTestFactory implements JmsListenerContainerFact private boolean autoStartup = true; - private final Map listenerContainers = - new LinkedHashMap<>(); + private final Map listenerContainers = new LinkedHashMap<>(); public void setAutoStartup(boolean autoStartup) { this.autoStartup = autoStartup; } - public List getListenerContainers() { return new ArrayList<>(this.listenerContainers.values()); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java index 527da9d9b000..6d16e8c1b6f0 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -70,13 +70,15 @@ public class SingleConnectionFactory extends DelegatingConnectionFactory private boolean suppressClose; /** Override auto-commit state?. */ - private @Nullable Boolean autoCommit; + @Nullable + private Boolean autoCommit; /** Wrapped Connection. */ private final AtomicReference target = new AtomicReference<>(); /** Proxy Connection. */ - private @Nullable Connection connection; + @Nullable + private Connection connection; private final Mono connectionEmitter; From 24a44870501d41a5b8587003081e3bb91c1ca7b7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 21 Feb 2024 22:45:32 +0100 Subject: [PATCH 102/261] Add test for cleanup after configuration class creation failure See gh-23343 --- .../ConfigurationClassPostProcessorTests.java | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 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 2c3a188ce20a..c205ac781ab9 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 @@ -35,6 +35,8 @@ import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; @@ -944,8 +946,8 @@ void testSelfReferenceExclusionForFactoryMethodOnSameBean() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfig.class)); beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); - beanFactory.preInstantiateSingletons(); + beanFactory.preInstantiateSingletons(); beanFactory.getBean(ServiceBean.class); } @@ -958,8 +960,8 @@ void testConfigWithDefaultMethods() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithDefaultMethods.class)); beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); - beanFactory.preInstantiateSingletons(); + beanFactory.preInstantiateSingletons(); beanFactory.getBean(ServiceBean.class); } @@ -972,11 +974,25 @@ void testConfigWithDefaultMethodsUsingAsm() { beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithDefaultMethods.class.getName())); beanFactory.registerBeanDefinition("serviceBeanProvider", new RootBeanDefinition(ServiceBeanProvider.class.getName())); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); - beanFactory.preInstantiateSingletons(); + beanFactory.preInstantiateSingletons(); beanFactory.getBean(ServiceBean.class); } + @Test + void testConfigWithFailingInit() { // gh-23343 + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(beanFactory); + beanFactory.addBeanPostProcessor(bpp); + beanFactory.addBeanPostProcessor(new CommonAnnotationBeanPostProcessor()); + beanFactory.registerBeanDefinition("configClass", new RootBeanDefinition(ConcreteConfigWithFailingInit.class)); + new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(beanFactory::preInstantiateSingletons); + assertThat(beanFactory.containsSingleton("configClass")).isFalse(); + assertThat(beanFactory.containsSingleton("provider")).isFalse(); + } + @Test void testCircularDependency() { AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); @@ -985,6 +1001,7 @@ void testCircularDependency() { beanFactory.registerBeanDefinition("configClass1", new RootBeanDefinition(A.class)); beanFactory.registerBeanDefinition("configClass2", new RootBeanDefinition(AStrich.class)); new ConfigurationClassPostProcessor().postProcessBeanFactory(beanFactory); + assertThatExceptionOfType(BeanCreationException.class) .isThrownBy(beanFactory::preInstantiateSingletons) .withMessageContaining("Circular reference"); @@ -1768,6 +1785,29 @@ public void validate() { } } + @Configuration + public static class ConcreteConfigWithFailingInit implements DefaultMethodsConfig, BeanFactoryAware { + + private BeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Bean + @Override + public ServiceBeanProvider provider() { + return new ServiceBeanProvider(); + } + + @PostConstruct + public void validate() { + beanFactory.getBean("provider"); + throw new IllegalStateException(); + } + } + @Primary public static class ServiceBeanProvider { From 5187281b50067bd663285f75fc993da915245edc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 21 Feb 2024 22:45:39 +0100 Subject: [PATCH 103/261] Polishing --- .../factory/support/ConstructorResolver.java | 11 +++----- .../support/DefaultSingletonBeanRegistry.java | 12 ++++----- .../annotation/ConfigurationClass.java | 27 +++++++++---------- 3 files changed, 22 insertions(+), 28 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 df91c0ca0895..e14d6448c605 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -609,13 +609,10 @@ else if (resolvedValues != null) { String argDesc = StringUtils.collectionToCommaDelimitedString(argTypes); throw new BeanCreationException(mbd.getResourceDescription(), beanName, "No matching factory method found on class [" + factoryClass.getName() + "]: " + - (mbd.getFactoryBeanName() != null ? - "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + + (mbd.getFactoryBeanName() != null ? "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") + "factory method '" + mbd.getFactoryMethodName() + "(" + argDesc + ")'. " + - "Check that a method with the specified name " + - (minNrOfArgs > 0 ? "and arguments " : "") + - "exists and that it is " + - (isStatic ? "static" : "non-static") + "."); + "Check that a method with the specified name " + (minNrOfArgs > 0 ? "and arguments " : "") + + "exists and that it is " + (isStatic ? "static" : "non-static") + "."); } else if (void.class == factoryMethodToUse.getReturnType()) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 9b189b34312d..81e442404973 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -567,16 +567,16 @@ public void destroySingleton(String beanName) { */ protected void destroyBean(String beanName, @Nullable DisposableBean bean) { // Trigger destruction of dependent beans first... - Set dependencies; + Set dependentBeanNames; synchronized (this.dependentBeanMap) { // Within full synchronization in order to guarantee a disconnected Set - dependencies = this.dependentBeanMap.remove(beanName); + dependentBeanNames = this.dependentBeanMap.remove(beanName); } - if (dependencies != null) { + if (dependentBeanNames != null) { if (logger.isTraceEnabled()) { - logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependencies); + logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependentBeanNames); } - for (String dependentBeanName : dependencies) { + for (String dependentBeanName : dependentBeanNames) { destroySingleton(dependentBeanName); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index e462008c7c2f..f952bf666ba1 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -73,7 +73,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param metadataReader reader used to parse the underlying {@link Class} * @param beanName must not be {@code null} - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(MetadataReader metadataReader, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -87,10 +86,10 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if importedBy is not {@code null}). * @param metadataReader reader used to parse the underlying {@link Class} - * @param importedBy the configuration class importing this one or {@code null} + * @param importedBy the configuration class importing this one * @since 3.1.1 */ - ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) { + ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) { this.metadata = metadataReader.getAnnotationMetadata(); this.resource = metadataReader.getResource(); this.importedBy.add(importedBy); @@ -100,7 +99,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param clazz the underlying {@link Class} to represent * @param beanName name of the {@code @Configuration} class bean - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(Class clazz, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -114,10 +112,10 @@ final class ConfigurationClass { * using the {@link Import} annotation or automatically processed as a nested * configuration class (if imported is {@code true}). * @param clazz the underlying {@link Class} to represent - * @param importedBy the configuration class importing this one (or {@code null}) + * @param importedBy the configuration class importing this one * @since 3.1.1 */ - ConfigurationClass(Class clazz, @Nullable ConfigurationClass importedBy) { + ConfigurationClass(Class clazz, ConfigurationClass importedBy) { this.metadata = AnnotationMetadata.introspect(clazz); this.resource = new DescriptiveResource(clazz.getName()); this.importedBy.add(importedBy); @@ -127,7 +125,6 @@ final class ConfigurationClass { * Create a new {@link ConfigurationClass} with the given name. * @param metadata the metadata for the underlying class to represent * @param beanName name of the {@code @Configuration} class bean - * @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass) */ ConfigurationClass(AnnotationMetadata metadata, String beanName) { Assert.notNull(beanName, "Bean name must not be null"); @@ -149,12 +146,12 @@ String getSimpleName() { return ClassUtils.getShortName(getMetadata().getClassName()); } - void setBeanName(String beanName) { + void setBeanName(@Nullable String beanName) { this.beanName = beanName; } @Nullable - public String getBeanName() { + String getBeanName() { return this.beanName; } @@ -164,7 +161,7 @@ public String getBeanName() { * @since 3.1.1 * @see #getImportedBy() */ - public boolean isImported() { + boolean isImported() { return !this.importedBy.isEmpty(); } @@ -198,6 +195,10 @@ void addImportedResource(String importedResource, Class> getImportedResources() { + return this.importedResources; + } + void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) { this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata); } @@ -206,10 +207,6 @@ Map getImportBeanDefinitionRe return this.importBeanDefinitionRegistrars; } - Map> getImportedResources() { - return this.importedResources; - } - void validate(ProblemReporter problemReporter) { Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); From 429c477f6a8df557dcb925cece95864edce5e5a3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 22 Feb 2024 16:56:22 +0100 Subject: [PATCH 104/261] Polishing --- .../IncorrectResultSetColumnCountException.java | 4 ++-- ...dateAffectedIncorrectNumberOfRowsException.java | 4 ++-- .../InterruptibleBatchPreparedStatementSetter.java | 4 ++-- .../jdbc/object/RdbmsOperation.java | 8 ++++---- .../jdbc/object/StoredProcedure.java | 4 ++-- .../CustomSQLExceptionTranslatorRegistry.java | 10 +++++----- .../springframework/jdbc/support/JdbcUtils.java | 14 +++++++------- .../jdbc/support/SQLErrorCodesFactory.java | 4 ++-- ...ncorrectUpdateSemanticsDataAccessException.java | 7 ++++--- 9 files changed, 30 insertions(+), 29 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java index 3cc779b19766..562140faf025 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/IncorrectResultSetColumnCountException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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,7 @@ /** * Data access exception thrown when a result set did not have the correct column count, - * for example when expecting a single column but getting 0 or more than 1 columns. + * for example when expecting a single column but getting 0 or more than 1 column. * * @author Juergen Hoeller * @since 2.0 diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java index 4b88b4159fa8..d7549b0f5cfd 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/JdbcUpdateAffectedIncorrectNumberOfRowsException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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,7 @@ /** * Exception thrown when a JDBC update affects an unexpected number of rows. - * Typically we expect an update to affect a single row, meaning it's an + * Typically, we expect an update to affect a single row, meaning it is an * error if it affects multiple rows. * * @author Rod Johnson diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java index 9de0357d6415..9a304e1cf115 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/InterruptibleBatchPreparedStatementSetter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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. @@ -22,7 +22,7 @@ * *

    This interface allows you to signal the end of a batch rather than * having to determine the exact batch size upfront. Batch size is still - * being honored but it is now the maximum size of the batch. + * being honored, but it is now the maximum size of the batch. * *

    The {@link #isBatchExhausted} method is called after each call to * {@link #setValues} to determine whether there were some values added, diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java index cba88a46040b..fe7b05b11131 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -128,8 +128,8 @@ public void setFetchSize(int fetchSize) { /** * Set the maximum number of rows for this RDBMS operation. This is important - * for processing subsets of large result sets, avoiding to read and hold - * the entire result set in the database or in the JDBC driver. + * for processing subsets of large result sets, in order to avoid reading and + * holding the entire result set in the database or in the JDBC driver. *

    Default is -1, indicating to use the driver's default. * @see org.springframework.jdbc.core.JdbcTemplate#setMaxRows */ @@ -175,7 +175,7 @@ public int getResultSetType() { public void setUpdatableResults(boolean updatableResults) { if (isCompiled()) { throw new InvalidDataAccessApiUsageException( - "The updateableResults flag must be set before the operation is compiled"); + "The updatableResults flag must be set before the operation is compiled"); } this.updatableResults = updatableResults; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java index f740eb4b792f..d860eb206bdc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/StoredProcedure.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,7 +29,7 @@ /** * Superclass for object abstractions of RDBMS stored procedures. - * This class is abstract and it is intended that subclasses will provide a typed + * This class is abstract, and it is intended that subclasses will provide a typed * method for invocation that delegates to the supplied {@link #execute} method. * *

    The inherited {@link #setSql sql} property is the name of the stored procedure diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java index 2908e65bac3f..259353a04f73 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -25,9 +25,9 @@ import org.springframework.lang.Nullable; /** - * Registry for custom {@link org.springframework.jdbc.support.SQLExceptionTranslator} instances associated with - * specific databases allowing for overriding translation based on values contained in the configuration file - * named "sql-error-codes.xml". + * Registry for custom {@link SQLExceptionTranslator} instances associated with + * specific databases allowing for overriding translation based on values + * contained in the configuration file named "sql-error-codes.xml". * * @author Thomas Risberg * @since 3.1.1 @@ -38,7 +38,7 @@ public final class CustomSQLExceptionTranslatorRegistry { private static final Log logger = LogFactory.getLog(CustomSQLExceptionTranslatorRegistry.class); /** - * Keep track of a single instance so we can return it to classes that request it. + * Keep track of a single instance, so we can return it to classes that request it. */ private static final CustomSQLExceptionTranslatorRegistry instance = new CustomSQLExceptionTranslatorRegistry(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java index fb42d833a4f2..3c6580d0d458 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -219,7 +219,7 @@ else if (obj instanceof Number number) { return NumberUtils.convertNumberToTargetClass(number, Integer.class); } else { - // e.g. on Postgres: getObject returns a PGObject but we need a String + // e.g. on Postgres: getObject returns a PGObject, but we need a String return rs.getString(index); } } @@ -415,14 +415,14 @@ public static T extractDatabaseMetaData(DataSource dataSource, final String } /** - * Return whether the given JDBC driver supports JDBC 2.0 batch updates. + * Return whether the given JDBC driver supports JDBC batch updates. *

    Typically invoked right before execution of a given set of statements: * to decide whether the set of SQL statements should be executed through - * the JDBC 2.0 batch mechanism or simply in a traditional one-by-one fashion. + * the JDBC batch mechanism or simply in a traditional one-by-one fashion. *

    Logs a warning if the "supportsBatchUpdates" methods throws an exception * and simply returns {@code false} in that case. * @param con the Connection to check - * @return whether JDBC 2.0 batch updates are supported + * @return whether JDBC batch updates are supported * @see java.sql.DatabaseMetaData#supportsBatchUpdates() */ public static boolean supportsBatchUpdates(Connection con) { @@ -492,8 +492,8 @@ public static String resolveTypeName(int sqlType) { /** * Determine the column name to use. The column name is determined based on a * lookup using ResultSetMetaData. - *

    This method implementation takes into account recent clarifications - * expressed in the JDBC 4.0 specification: + *

    This method's implementation takes into account clarifications expressed + * in the JDBC 4.0 specification: *

    columnLabel - the label for the column specified with the SQL AS clause. * If the SQL AS clause was not specified, then the label is the name of the column. * @param resultSetMetaData the current meta-data to use diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java index 12e8ba6cc3cf..66759edeeec5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -67,7 +67,7 @@ public class SQLErrorCodesFactory { private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class); /** - * Keep track of a single instance so we can return it to classes that request it. + * Keep track of a single instance, so we can return it to classes that request it. * Lazily initialized in order to avoid making {@code SQLErrorCodesFactory} constructor * reachable on native images when not needed. */ diff --git a/spring-tx/src/main/java/org/springframework/dao/IncorrectUpdateSemanticsDataAccessException.java b/spring-tx/src/main/java/org/springframework/dao/IncorrectUpdateSemanticsDataAccessException.java index 70689977cd2e..2eb1749da9e9 100644 --- a/spring-tx/src/main/java/org/springframework/dao/IncorrectUpdateSemanticsDataAccessException.java +++ b/spring-tx/src/main/java/org/springframework/dao/IncorrectUpdateSemanticsDataAccessException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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. @@ -44,10 +44,11 @@ public IncorrectUpdateSemanticsDataAccessException(String msg, Throwable cause) super(msg, cause); } + /** * Return whether data was updated. - * If this method returns false, there's nothing to roll back. - *

    The default implementation always returns true. + * If this method returns {@code false}, there is nothing to roll back. + *

    The default implementation always returns {@code true}. * This can be overridden in subclasses. */ public boolean wasDataUpdated() { From 7686f5467e5b4b6ef27e9fa97990f2962b9bcc90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 22 Feb 2024 17:06:53 +0100 Subject: [PATCH 105/261] Adapt Hibernate native support for HHH-17643 This commit adapts Hibernate native support to handle the changes performed as part of HHH-17643 which impacts Hibernate versions 6.4.3+ and 6.2.21+. It ignores the BytecodeProvider services loaded by the service loader feature in order to default to the "no-op" provider with native. gh-32314 is expected to remove the need for such substitutions which are not great for maintainability by design. Closes gh-32312 --- .../org.springframework/spring-orm/native-image.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 spring-orm/src/main/resources/META-INF/native-image/org.springframework/spring-orm/native-image.properties diff --git a/spring-orm/src/main/resources/META-INF/native-image/org.springframework/spring-orm/native-image.properties b/spring-orm/src/main/resources/META-INF/native-image/org.springframework/spring-orm/native-image.properties new file mode 100644 index 000000000000..ae2375f26451 --- /dev/null +++ b/spring-orm/src/main/resources/META-INF/native-image/org.springframework/spring-orm/native-image.properties @@ -0,0 +1 @@ +Args = -H:ServiceLoaderFeatureExcludeServices=org.hibernate.bytecode.spi.BytecodeProvider From 9a6f636e17dc67512debc132eaed29acb721af41 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 24 Feb 2024 08:21:37 +0100 Subject: [PATCH 106/261] Consistent nullability for internal field access --- .../request/async/WebAsyncManager.java | 48 ++++++++++--------- .../context/request/async/WebAsyncTask.java | 6 ++- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 220996ee7056..a94fbd3900ec 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -135,8 +135,8 @@ public void setTaskExecutor(AsyncTaskExecutor taskExecutor) { } /** - * Whether the selected handler for the current request chose to handle the - * request asynchronously. A return value of "true" indicates concurrent + * Return whether the selected handler for the current request chose to handle + * the request asynchronously. A return value of "true" indicates concurrent * handling is under way and the response will remain open. A return value * of "false" means concurrent handling was either not started or possibly * that it has completed and the request was dispatched for further @@ -147,16 +147,16 @@ public boolean isConcurrentHandlingStarted() { } /** - * Whether a result value exists as a result of concurrent handling. + * Return whether a result value exists as a result of concurrent handling. */ public boolean hasConcurrentResult() { return (this.concurrentResult != RESULT_NONE); } /** - * Provides access to the result from concurrent handling. + * Get the result from concurrent handling. * @return an Object, possibly an {@code Exception} or {@code Throwable} if - * concurrent handling raised one. + * concurrent handling raised one * @see #clearConcurrentResult() */ @Nullable @@ -165,8 +165,7 @@ public Object getConcurrentResult() { } /** - * Provides access to additional processing context saved at the start of - * concurrent handling. + * Get the additional processing context saved at the start of concurrent handling. * @see #clearConcurrentResult() */ @Nullable @@ -207,7 +206,7 @@ public void registerCallableInterceptor(Object key, CallableProcessingIntercepto /** * Register a {@link CallableProcessingInterceptor} without a key. - * The key is derived from the class name and hashcode. + * The key is derived from the class name and hash code. * @param interceptors one or more interceptors to register */ public void registerCallableInterceptors(CallableProcessingInterceptor... interceptors) { @@ -230,8 +229,8 @@ public void registerDeferredResultInterceptor(Object key, DeferredResultProcessi } /** - * Register one or more {@link DeferredResultProcessingInterceptor DeferredResultProcessingInterceptors} without a specified key. - * The default key is derived from the interceptor class name and hash code. + * Register one or more {@link DeferredResultProcessingInterceptor DeferredResultProcessingInterceptors} + * without a specified key. The default key is derived from the interceptor class name and hash code. * @param interceptors one or more interceptors to register */ public void registerDeferredResultInterceptors(DeferredResultProcessingInterceptor... interceptors) { @@ -297,7 +296,7 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. this.taskExecutor = executor; } else { - logExecutorWarning(); + logExecutorWarning(this.asyncWebRequest); } List interceptors = new ArrayList<>(); @@ -310,7 +309,7 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. this.asyncWebRequest.addTimeoutHandler(() -> { if (logger.isDebugEnabled()) { - logger.debug("Async request timeout for " + formatRequestUri()); + logger.debug("Async request timeout for " + formatUri(this.asyncWebRequest)); } Object result = interceptorChain.triggerAfterTimeout(this.asyncWebRequest, callable); if (result != CallableProcessingInterceptor.RESULT_NONE) { @@ -321,7 +320,7 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. this.asyncWebRequest.addErrorHandler(ex -> { if (!this.errorHandlingInProgress) { if (logger.isDebugEnabled()) { - logger.debug("Async request error for " + formatRequestUri() + ": " + ex); + logger.debug("Async request error for " + formatUri(this.asyncWebRequest) + ": " + ex); } Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex); result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex); @@ -358,7 +357,7 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. } } - private void logExecutorWarning() { + private void logExecutorWarning(AsyncWebRequest asyncWebRequest) { if (taskExecutorWarning && logger.isWarnEnabled()) { synchronized (DEFAULT_TASK_EXECUTOR) { AsyncTaskExecutor executor = this.taskExecutor; @@ -370,7 +369,7 @@ private void logExecutorWarning() { "Please, configure a TaskExecutor in the MVC config under \"async support\".\n" + "The " + executorTypeName + " currently in use is not suitable under load.\n" + "-------------------------------\n" + - "Request URI: '" + formatRequestUri() + "'\n" + + "Request URI: '" + formatUri(asyncWebRequest) + "'\n" + "!!!"); taskExecutorWarning = false; } @@ -378,11 +377,6 @@ private void logExecutorWarning() { } } - private String formatRequestUri() { - HttpServletRequest request = this.asyncWebRequest.getNativeRequest(HttpServletRequest.class); - return request != null ? request.getRequestURI() : "servlet container"; - } - private void setConcurrentResultAndDispatch(@Nullable Object result) { synchronized (WebAsyncManager.this) { if (this.concurrentResult != RESULT_NONE) { @@ -392,16 +386,18 @@ private void setConcurrentResultAndDispatch(@Nullable Object result) { this.errorHandlingInProgress = (result instanceof Throwable); } + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); if (this.asyncWebRequest.isAsyncComplete()) { if (logger.isDebugEnabled()) { - logger.debug("Async result set but request already complete: " + formatRequestUri()); + logger.debug("Async result set but request already complete: " + formatUri(this.asyncWebRequest)); } return; } if (logger.isDebugEnabled()) { boolean isError = result instanceof Throwable; - logger.debug("Async " + (isError ? "error" : "result set") + ", dispatch to " + formatRequestUri()); + logger.debug("Async " + (isError ? "error" : "result set") + + ", dispatch to " + formatUri(this.asyncWebRequest)); } this.asyncWebRequest.dispatch(); } @@ -485,11 +481,17 @@ private void startAsyncProcessing(Object[] processingContext) { this.concurrentResultContext = processingContext; this.errorHandlingInProgress = false; } - this.asyncWebRequest.startAsync(); + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); + this.asyncWebRequest.startAsync(); if (logger.isDebugEnabled()) { logger.debug("Started async request"); } } + private static String formatUri(AsyncWebRequest asyncWebRequest) { + HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class); + return (request != null ? request.getRequestURI() : "servlet container"); + } + } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncTask.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncTask.java index d105446bf3d5..5b49b79d466f 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncTask.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -47,12 +47,16 @@ public class WebAsyncTask implements BeanFactoryAware { @Nullable private final String executorName; + @Nullable private BeanFactory beanFactory; + @Nullable private Callable timeoutCallback; + @Nullable private Callable errorCallback; + @Nullable private Runnable completionCallback; From e9bf5f55694b1eadbad8caf5221d689de85ab2fc Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:34:48 +0100 Subject: [PATCH 107/261] Polish ContentCachingResponseWrapper[Tests] --- .../util/ContentCachingResponseWrapper.java | 19 ++++++---- .../ContentCachingResponseWrapperTests.java | 37 +++++++++++++------ 2 files changed, 36 insertions(+), 20 deletions(-) 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 4223a673976c..017c0f635628 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 @@ -38,7 +38,7 @@ /** * {@link jakarta.servlet.http.HttpServletResponse} wrapper that caches all content written to * the {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer}, - * and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}. + * and allows this content to be retrieved via a {@linkplain #getContentAsByteArray() byte array}. * *

    Used e.g. by {@link org.springframework.web.filter.ShallowEtagHeaderFilter}. * @@ -120,9 +120,16 @@ public PrintWriter getWriter() throws IOException { return this.writer; } + /** + * This method neither flushes content to the client nor commits the underlying + * response, since the content has not yet been copied to the response. + *

    Invoke {@link #copyBodyToResponse()} to copy the cached body content to + * the wrapped response object and flush its buffer. + * @see jakarta.servlet.ServletResponseWrapper#flushBuffer() + */ @Override public void flushBuffer() throws IOException { - // do not flush the underlying response as the content has not been copied to it yet + // no-op } @Override @@ -139,15 +146,11 @@ public void setContentLengthLong(long len) { throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" + Integer.MAX_VALUE + "): " + len); } - int lenInt = (int) len; - if (lenInt > this.content.size()) { - this.content.resize(lenInt); - } - this.contentLength = lenInt; + setContentLength((int) len); } @Override - public void setContentType(String type) { + public void setContentType(@Nullable String type) { this.contentType = type; } 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 index eab4aaa0812c..1c11b5a969b1 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java @@ -16,17 +16,17 @@ package org.springframework.web.filter; -import java.nio.charset.StandardCharsets; - import jakarta.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 java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; +import static org.springframework.http.HttpHeaders.TRANSFER_ENCODING; /** * Unit tests for {@link ContentCachingResponseWrapper}. @@ -36,34 +36,47 @@ public class ContentCachingResponseWrapperTests { @Test void copyBodyToResponse() throws Exception { - byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); MockHttpServletResponse response = new MockHttpServletResponse(); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); - responseWrapper.setStatus(HttpServletResponse.SC_OK); + responseWrapper.setStatus(HttpServletResponse.SC_CREATED); FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); responseWrapper.copyBodyToResponse(); - assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); assertThat(response.getContentLength()).isGreaterThan(0); 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); + byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(UTF_8); MockHttpServletResponse response = new MockHttpServletResponse(); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); - responseWrapper.setStatus(HttpServletResponse.SC_OK); - responseWrapper.setHeader(HttpHeaders.TRANSFER_ENCODING, "chunked"); + responseWrapper.setStatus(HttpServletResponse.SC_CREATED); + responseWrapper.setHeader(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.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertHeader(response, TRANSFER_ENCODING, "chunked"); + assertHeader(response, CONTENT_LENGTH, null); assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); } + private void assertHeader(HttpServletResponse response, String header, String value) { + if (value == null) { + assertThat(response.containsHeader(header)).as(header).isFalse(); + assertThat(response.getHeader(header)).as(header).isNull(); + assertThat(response.getHeaders(header)).as(header).isEmpty(); + } + else { + assertThat(response.containsHeader(header)).as(header).isTrue(); + assertThat(response.getHeader(header)).as(header).isEqualTo(value); + assertThat(response.getHeaders(header)).as(header).containsExactly(value); + } + } + } From ca602ef8743168f4db49488c3778b3d897d780ce Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 25 Feb 2024 17:26:50 +0100 Subject: [PATCH 108/261] Honor Content-[Type|Length] headers from wrapped response again Commit 375e0e6827 introduced a regression in ContentCachingResponseWrapper (CCRW). Specifically, CCRW no longer honors Content-Type and Content-Length headers that have been set in the wrapped response and now incorrectly returns null for those header values if they have not been set directly in the CCRW. This commit fixes this regression as follows. - The Content-Type and Content-Length headers set in the wrapped response are honored in getContentType(), containsHeader(), getHeader(), and getHeaders() unless those headers have been set directly in the CCRW. - In copyBodyToResponse(), the Content-Type in the wrapped response is only overridden if the Content-Type has been set directly in the CCRW. Furthermore, prior to this commit, getHeaderNames() returned duplicates for the Content-Type and Content-Length headers if they were set in the wrapped response as well as in CCRW. This commit fixes that by returning a unique set from getHeaderNames(). This commit also updates ContentCachingResponseWrapperTests to verify the expected behavior for Content-Type and Content-Length headers that are set in the wrapped response as well as in CCRW. See gh-32039 See gh-32317 Closes gh-32321 --- .../util/ContentCachingResponseWrapper.java | 37 ++--- .../ContentCachingResponseWrapperTests.java | 138 ++++++++++++++++++ 2 files changed, 158 insertions(+), 17 deletions(-) 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 017c0f635628..2462876a2c6f 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 @@ -21,10 +21,10 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; +import java.util.LinkedHashSet; +import java.util.Set; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.WriteListener; @@ -43,6 +43,7 @@ *

    Used e.g. by {@link org.springframework.web.filter.ShallowEtagHeaderFilter}. * * @author Juergen Hoeller + * @author Sam Brannen * @since 4.1.3 * @see ContentCachingRequestWrapper */ @@ -157,16 +158,19 @@ public void setContentType(@Nullable String type) { @Override @Nullable public String getContentType() { - return this.contentType; + if (this.contentType != null) { + return this.contentType; + } + return super.getContentType(); } @Override public boolean containsHeader(String name) { - if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { - return this.contentLength != null; + if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return true; } - else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { - return this.contentType != null; + else if (this.contentType != null && HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + return true; } else { return super.containsHeader(name); @@ -222,10 +226,10 @@ public void addIntHeader(String name, int value) { @Override @Nullable public String getHeader(String name) { - if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { - return (this.contentLength != null) ? this.contentLength.toString() : null; + if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return this.contentLength.toString(); } - else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + else if (this.contentType != null && HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { return this.contentType; } else { @@ -235,12 +239,11 @@ else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { @Override public Collection getHeaders(String name) { - if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { - return this.contentLength != null ? Collections.singleton(this.contentLength.toString()) : - Collections.emptySet(); + if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + return Collections.singleton(this.contentLength.toString()); } - else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { - return this.contentType != null ? Collections.singleton(this.contentType) : Collections.emptySet(); + else if (this.contentType != null && HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + return Collections.singleton(this.contentType); } else { return super.getHeaders(name); @@ -251,7 +254,7 @@ else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { public Collection getHeaderNames() { Collection headerNames = super.getHeaderNames(); if (this.contentLength != null || this.contentType != null) { - List result = new ArrayList<>(headerNames); + Set result = new LinkedHashSet<>(headerNames); if (this.contentLength != null) { result.add(HttpHeaders.CONTENT_LENGTH); } @@ -330,7 +333,7 @@ protected void copyBodyToResponse(boolean complete) throws IOException { } this.contentLength = null; } - if (complete || this.contentType != null) { + if (this.contentType != null) { rawResponse.setContentType(this.contentType); this.contentType = null; } 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 index 1c11b5a969b1..ec586ae3da79 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java @@ -16,21 +16,32 @@ package org.springframework.web.filter; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import org.springframework.web.util.ContentCachingResponseWrapper; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; +import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.HttpHeaders.TRANSFER_ENCODING; /** * Unit tests for {@link ContentCachingResponseWrapper}. * @author Rossen Stoyanchev + * @author Sam Brannen */ public class ContentCachingResponseWrapperTests { @@ -49,6 +60,124 @@ void copyBodyToResponse() throws Exception { assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); } + @Test + void copyBodyToResponseWithPresetHeaders() throws Exception { + String PUZZLE = "puzzle"; + String ENIGMA = "enigma"; + String NUMBER = "number"; + String MAGIC = "42"; + + byte[] responseBody = "Hello World".getBytes(UTF_8); + int responseLength = responseBody.length; + int originalContentLength = 999; + String contentType = MediaType.APPLICATION_JSON_VALUE; + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(contentType); + response.setContentLength(originalContentLength); + response.setHeader(PUZZLE, ENIGMA); + response.setIntHeader(NUMBER, 42); + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + responseWrapper.setStatus(HttpServletResponse.SC_CREATED); + + assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()) + .containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(responseWrapper, PUZZLE, ENIGMA); + assertHeader(responseWrapper, NUMBER, MAGIC); + assertHeader(responseWrapper, CONTENT_LENGTH, originalContentLength); + assertContentTypeHeader(responseWrapper, contentType); + + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength); + + responseWrapper.copyBodyToResponse(); + + assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()) + .containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(responseWrapper, PUZZLE, ENIGMA); + assertHeader(responseWrapper, NUMBER, MAGIC); + assertHeader(responseWrapper, CONTENT_LENGTH, responseLength); + assertContentTypeHeader(responseWrapper, contentType); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(response.getContentLength()).isEqualTo(responseLength); + assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + assertThat(response.getHeaderNames()) + .containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(response, PUZZLE, ENIGMA); + assertHeader(response, NUMBER, MAGIC); + assertHeader(response, CONTENT_LENGTH, responseLength); + assertContentTypeHeader(response, contentType); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("setContentTypeFunctions") + void copyBodyToResponseWithOverridingHeaders(BiConsumer setContentType) throws Exception { + byte[] responseBody = "Hello World".getBytes(UTF_8); + int responseLength = responseBody.length; + int originalContentLength = 11; + int overridingContentLength = 22; + String originalContentType = MediaType.TEXT_PLAIN_VALUE; + String overridingContentType = MediaType.APPLICATION_JSON_VALUE; + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentLength(originalContentLength); + response.setContentType(originalContentType); + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + responseWrapper.setStatus(HttpServletResponse.SC_CREATED); + responseWrapper.setContentLength(overridingContentLength); + setContentType.accept(responseWrapper, overridingContentType); + + assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(response, CONTENT_LENGTH, originalContentLength); + assertHeader(responseWrapper, CONTENT_LENGTH, overridingContentLength); + assertContentTypeHeader(response, originalContentType); + assertContentTypeHeader(responseWrapper, overridingContentType); + + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength); + + responseWrapper.copyBodyToResponse(); + + assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH); + + assertHeader(response, CONTENT_LENGTH, responseLength); + assertHeader(responseWrapper, CONTENT_LENGTH, responseLength); + assertContentTypeHeader(response, overridingContentType); + assertContentTypeHeader(responseWrapper, overridingContentType); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + assertThat(response.getContentLength()).isEqualTo(responseLength); + assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + assertThat(response.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH); + } + + private static Stream setContentTypeFunctions() { + return Stream.of( + namedArguments("setContentType()", HttpServletResponse::setContentType), + namedArguments("setHeader()", (response, contentType) -> response.setHeader(CONTENT_TYPE, contentType)), + namedArguments("addHeader()", (response, contentType) -> response.addHeader(CONTENT_TYPE, contentType)) + ); + } + + private static Arguments namedArguments(String name, BiConsumer setContentTypeFunction) { + return arguments(named(name, setContentTypeFunction)); + } + @Test void copyBodyToResponseWithTransferEncoding() throws Exception { byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(UTF_8); @@ -66,6 +195,10 @@ void copyBodyToResponseWithTransferEncoding() throws Exception { assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); } + private void assertHeader(HttpServletResponse response, String header, int value) { + assertHeader(response, header, Integer.toString(value)); + } + private void assertHeader(HttpServletResponse response, String header, String value) { if (value == null) { assertThat(response.containsHeader(header)).as(header).isFalse(); @@ -79,4 +212,9 @@ private void assertHeader(HttpServletResponse response, String header, String va } } + private void assertContentTypeHeader(HttpServletResponse response, String contentType) { + assertHeader(response, CONTENT_TYPE, contentType); + assertThat(response.getContentType()).as(CONTENT_TYPE).isEqualTo(contentType); + } + } From 7bf07ef393ea0e31c20e2fe19c680a18925e8aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 27 Feb 2024 15:29:37 +0100 Subject: [PATCH 109/261] Refine *HttpMessageConverter#getContentLength null safety Closes gh-32333 --- .../http/converter/ResourceHttpMessageConverter.java | 3 ++- .../converter/json/AbstractJackson2HttpMessageConverter.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java index 6eb6cea988a3..4f1f34cc8551 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -113,6 +113,7 @@ protected MediaType getDefaultContentType(Resource resource) { } @Override + @Nullable protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException { // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards... // Note: custom InputStreamResource subclasses could provide a pre-calculated content length! diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 9c585ab6ae5d..f64e66837335 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -561,6 +561,7 @@ protected MediaType getDefaultContentType(Object object) throws IOException { } @Override + @Nullable protected Long getContentLength(Object object, @Nullable MediaType contentType) throws IOException { if (object instanceof MappingJacksonValue mappingJacksonValue) { object = mappingJacksonValue.getValue(); From 629c56031659798876a868c2a6fcc47b27ee46fb Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:44:48 +0100 Subject: [PATCH 110/261] Polish ShallowEtagHeaderFilterTests --- .../filter/ShallowEtagHeaderFilterTests.java | 88 +++++++++---------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java index 3156b8b34760..a36cd55a62d1 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,32 +17,33 @@ package org.springframework.web.filter; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; /** * Tests for {@link ShallowEtagHeaderFilter}. + * * @author Arjen Poutsma * @author Brian Clozel * @author Juergen Hoeller */ -public class ShallowEtagHeaderFilterTests { +class ShallowEtagHeaderFilterTests { private final ShallowEtagHeaderFilter filter = new ShallowEtagHeaderFilter(); @Test - public void isEligibleForEtag() { + void isEligibleForEtag() { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -61,15 +62,15 @@ public void isEligibleForEtag() { } @Test - public void filterNoMatch() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterNoMatch() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); - filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE); + filterResponse.setContentType(TEXT_PLAIN_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); @@ -77,21 +78,21 @@ public void filterNoMatch() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(200); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0); - assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE); assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody); } @Test - public void filterNoMatchWeakETag() throws Exception { + void filterNoMatchWeakETag() throws Exception { this.filter.setWriteWeakETag(true); - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); - filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE); + filterResponse.setContentType(TEXT_PLAIN_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); @@ -99,22 +100,22 @@ public void filterNoMatchWeakETag() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(200); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("W/\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0); - assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE); assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody); } @Test - public void filterMatch() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterMatch() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; request.addHeader("If-None-Match", etag); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); - byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); filterResponse.setContentLength(responseBody.length); - filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE); + filterResponse.setContentType(TEXT_PLAIN_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; filter.doFilter(request, response, filterChain); @@ -123,20 +124,19 @@ public void filterMatch() throws Exception { assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse(); assertThat(response.containsHeader("Content-Type")).as("Response has Content-Type header").isFalse(); - byte[] expecteds = new byte[0]; - assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds); + assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty(); } @Test - public void filterMatchWeakEtag() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterMatchWeakEtag() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; request.addHeader("If-None-Match", "W/" + etag); MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); - byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); filterResponse.setContentLength(responseBody.length); }; @@ -145,13 +145,12 @@ public void filterMatchWeakEtag() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(304); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse(); - byte[] expecteds = new byte[0]; - assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds); + assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty(); } @Test - public void filterWriter() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterWriter() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\""; request.addHeader("If-None-Match", etag); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -167,16 +166,15 @@ public void filterWriter() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(304); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse(); - byte[] expecteds = new byte[0]; - assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds); + assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty(); } @Test // SPR-12960 - public void filterWriterWithDisabledCaching() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterWriterWithDisabledCaching() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); @@ -192,11 +190,11 @@ public void filterWriterWithDisabledCaching() throws Exception { } @Test - public void filterSendError() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterSendError() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); response.setContentLength(100); @@ -212,11 +210,11 @@ public void filterSendError() throws Exception { } @Test - public void filterSendErrorMessage() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterSendErrorMessage() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); response.setContentLength(100); @@ -233,11 +231,11 @@ public void filterSendErrorMessage() throws Exception { } @Test - public void filterSendRedirect() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterSendRedirect() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); response.setContentLength(100); @@ -254,11 +252,11 @@ public void filterSendRedirect() throws Exception { } @Test // SPR-13717 - public void filterFlushResponse() throws Exception { - final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + void filterFlushResponse() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); - final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8); + byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); From d1b3107398a2a4aa85bccf4a2df7efe07808732a Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:48:09 +0100 Subject: [PATCH 111/261] Do not cache Content-Type in ContentCachingResponseWrapper Based on feedback from several members of the community, we have decided to revert the caching of the Content-Type header that was introduced in ContentCachingResponseWrapper in 375e0e6827. This commit therefore completely removes Content-Type caching in ContentCachingResponseWrapper and updates the existing tests accordingly. To provide guards against future regressions in this area, this commit also introduces explicit tests for the 6 ways to set the content length in ContentCachingResponseWrapper and modifies a test in ShallowEtagHeaderFilterTests to ensure that a Content-Type header set directly on ContentCachingResponseWrapper is propagated to the underlying response even if content caching is disabled for the ShallowEtagHeaderFilter. See gh-32039 See gh-32317 Closes gh-32321 --- .../util/ContentCachingResponseWrapper.java | 45 +-------- .../ContentCachingResponseWrapperTests.java | 97 ++++++++++++++----- .../filter/ShallowEtagHeaderFilterTests.java | 7 +- 3 files changed, 81 insertions(+), 68 deletions(-) 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 2462876a2c6f..c2038fb0ecc2 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 @@ -60,9 +60,6 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper { @Nullable private Integer contentLength; - @Nullable - private String contentType; - /** * Create a new ContentCachingResponseWrapper for the given servlet response. @@ -150,28 +147,11 @@ public void setContentLengthLong(long len) { setContentLength((int) len); } - @Override - public void setContentType(@Nullable String type) { - this.contentType = type; - } - - @Override - @Nullable - public String getContentType() { - if (this.contentType != null) { - return this.contentType; - } - return super.getContentType(); - } - @Override public boolean containsHeader(String name) { if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { return true; } - else if (this.contentType != null && HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { - return true; - } else { return super.containsHeader(name); } @@ -182,9 +162,6 @@ public void setHeader(String name, String value) { if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { this.contentLength = Integer.valueOf(value); } - else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { - this.contentType = value; - } else { super.setHeader(name, value); } @@ -195,9 +172,6 @@ public void addHeader(String name, String value) { if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { this.contentLength = Integer.valueOf(value); } - else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { - this.contentType = value; - } else { super.addHeader(name, value); } @@ -229,9 +203,6 @@ public String getHeader(String name) { if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { return this.contentLength.toString(); } - else if (this.contentType != null && HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { - return this.contentType; - } else { return super.getHeader(name); } @@ -242,9 +213,6 @@ public Collection getHeaders(String name) { if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { return Collections.singleton(this.contentLength.toString()); } - else if (this.contentType != null && HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { - return Collections.singleton(this.contentType); - } else { return super.getHeaders(name); } @@ -253,14 +221,9 @@ else if (this.contentType != null && HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(n @Override public Collection getHeaderNames() { Collection headerNames = super.getHeaderNames(); - if (this.contentLength != null || this.contentType != null) { + if (this.contentLength != null) { Set result = new LinkedHashSet<>(headerNames); - if (this.contentLength != null) { - result.add(HttpHeaders.CONTENT_LENGTH); - } - if (this.contentType != null) { - result.add(HttpHeaders.CONTENT_TYPE); - } + result.add(HttpHeaders.CONTENT_LENGTH); return result; } else { @@ -333,10 +296,6 @@ protected void copyBodyToResponse(boolean complete) throws IOException { } this.contentLength = null; } - if (this.contentType != null) { - rawResponse.setContentType(this.contentType); - this.contentType = null; - } } this.content.writeTo(rawResponse.getOutputStream()); this.content.reset(); 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 index ec586ae3da79..091adb3e2c83 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ContentCachingResponseWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,13 +16,12 @@ package org.springframework.web.filter; -import java.util.function.BiConsumer; import java.util.stream.Stream; import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.http.MediaType; @@ -33,17 +32,17 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Named.named; -import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; import static org.springframework.http.HttpHeaders.TRANSFER_ENCODING; /** * Unit tests for {@link ContentCachingResponseWrapper}. + * * @author Rossen Stoyanchev * @author Sam Brannen */ -public class ContentCachingResponseWrapperTests { +class ContentCachingResponseWrapperTests { @Test void copyBodyToResponse() throws Exception { @@ -119,31 +118,76 @@ void copyBodyToResponseWithPresetHeaders() throws Exception { } @ParameterizedTest(name = "[{index}] {0}") - @MethodSource("setContentTypeFunctions") - void copyBodyToResponseWithOverridingHeaders(BiConsumer setContentType) throws Exception { + @MethodSource("setContentLengthFunctions") + void copyBodyToResponseWithOverridingContentLength(SetContentLength setContentLength) throws Exception { byte[] responseBody = "Hello World".getBytes(UTF_8); int responseLength = responseBody.length; int originalContentLength = 11; int overridingContentLength = 22; - String originalContentType = MediaType.TEXT_PLAIN_VALUE; - String overridingContentType = MediaType.APPLICATION_JSON_VALUE; MockHttpServletResponse response = new MockHttpServletResponse(); response.setContentLength(originalContentLength); - response.setContentType(originalContentType); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); - responseWrapper.setStatus(HttpServletResponse.SC_CREATED); responseWrapper.setContentLength(overridingContentLength); - setContentType.accept(responseWrapper, overridingContentType); - assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED); + setContentLength.invoke(responseWrapper, overridingContentLength); + assertThat(responseWrapper.getContentSize()).isZero(); - assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH); assertHeader(response, CONTENT_LENGTH, originalContentLength); assertHeader(responseWrapper, CONTENT_LENGTH, overridingContentLength); + + FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); + assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength); + + responseWrapper.copyBodyToResponse(); + + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH); + + assertHeader(response, CONTENT_LENGTH, responseLength); + assertHeader(responseWrapper, CONTENT_LENGTH, responseLength); + + assertThat(response.getContentLength()).isEqualTo(responseLength); + assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); + assertThat(response.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH); + } + + private static Stream> setContentLengthFunctions() { + return Stream.of( + named("setContentLength()", HttpServletResponse::setContentLength), + named("setContentLengthLong()", HttpServletResponse::setContentLengthLong), + named("setIntHeader()", (response, contentLength) -> response.setIntHeader(CONTENT_LENGTH, contentLength)), + named("addIntHeader()", (response, contentLength) -> response.addIntHeader(CONTENT_LENGTH, contentLength)), + named("setHeader()", (response, contentLength) -> response.setHeader(CONTENT_LENGTH, "" + contentLength)), + named("addHeader()", (response, contentLength) -> response.addHeader(CONTENT_LENGTH, "" + contentLength)) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("setContentTypeFunctions") + void copyBodyToResponseWithOverridingContentType(SetContentType setContentType) throws Exception { + byte[] responseBody = "Hello World".getBytes(UTF_8); + int responseLength = responseBody.length; + String originalContentType = MediaType.TEXT_PLAIN_VALUE; + String overridingContentType = MediaType.APPLICATION_JSON_VALUE; + + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(originalContentType); + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + assertContentTypeHeader(response, originalContentType); + assertContentTypeHeader(responseWrapper, originalContentType); + + setContentType.invoke(responseWrapper, overridingContentType); + + assertThat(responseWrapper.getContentSize()).isZero(); + assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE); + + assertContentTypeHeader(response, overridingContentType); assertContentTypeHeader(responseWrapper, overridingContentType); FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream()); @@ -151,7 +195,6 @@ void copyBodyToResponseWithOverridingHeaders(BiConsumer setContentTypeFunctions() { + private static Stream> setContentTypeFunctions() { return Stream.of( - namedArguments("setContentType()", HttpServletResponse::setContentType), - namedArguments("setHeader()", (response, contentType) -> response.setHeader(CONTENT_TYPE, contentType)), - namedArguments("addHeader()", (response, contentType) -> response.addHeader(CONTENT_TYPE, contentType)) + named("setContentType()", HttpServletResponse::setContentType), + named("setHeader()", (response, contentType) -> response.setHeader(CONTENT_TYPE, contentType)), + named("addHeader()", (response, contentType) -> response.addHeader(CONTENT_TYPE, contentType)) ); } - private static Arguments namedArguments(String name, BiConsumer setContentTypeFunction) { - return arguments(named(name, setContentTypeFunction)); - } - @Test void copyBodyToResponseWithTransferEncoding() throws Exception { byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(UTF_8); @@ -217,4 +255,15 @@ private void assertContentTypeHeader(HttpServletResponse response, String conten assertThat(response.getContentType()).as(CONTENT_TYPE).isEqualTo(contentType); } + + @FunctionalInterface + private interface SetContentLength { + void invoke(HttpServletResponse response, int contentLength); + } + + @FunctionalInterface + private interface SetContentType { + void invoke(HttpServletResponse response, String contentType); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java index a36cd55a62d1..aa153146869a 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java @@ -28,6 +28,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; /** @@ -36,6 +37,7 @@ * @author Arjen Poutsma * @author Brian Clozel * @author Juergen Hoeller + * @author Sam Brannen */ class ShallowEtagHeaderFilterTests { @@ -123,7 +125,7 @@ void filterMatch() throws Exception { assertThat(response.getStatus()).as("Invalid status").isEqualTo(304); assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\""); assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse(); - assertThat(response.containsHeader("Content-Type")).as("Response has Content-Type header").isFalse(); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE); assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty(); } @@ -173,11 +175,13 @@ void filterWriter() throws Exception { void filterWriterWithDisabledCaching() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(TEXT_PLAIN_VALUE); byte[] responseBody = "Hello World".getBytes(UTF_8); FilterChain filterChain = (filterRequest, filterResponse) -> { assertThat(filterRequest).as("Invalid request passed").isEqualTo(request); ((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK); + filterResponse.setContentType(APPLICATION_JSON_VALUE); FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); }; @@ -186,6 +190,7 @@ void filterWriterWithDisabledCaching() throws Exception { assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getHeader("ETag")).isNull(); + assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(APPLICATION_JSON_VALUE); assertThat(response.getContentAsByteArray()).isEqualTo(responseBody); } From b44ef70bc3fb11a6aa51762f63f898b3ed07dfcb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 19:14:30 +0100 Subject: [PATCH 112/261] Direct reference to PushBuilder API on Servlet 5.0 baseline See gh-29435 --- .../ServletRequestMethodArgumentResolver.java | 47 ++++--------------- 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java index 9da95ef51164..ba38ca86a463 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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,7 +32,6 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.WebRequest; @@ -68,21 +67,6 @@ */ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver { - @Nullable - private static Class pushBuilder; - - static { - try { - pushBuilder = ClassUtils.forName("jakarta.servlet.http.PushBuilder", - ServletRequestMethodArgumentResolver.class.getClassLoader()); - } - catch (ClassNotFoundException ex) { - // Servlet 4.0 PushBuilder not found - not supported for injection - pushBuilder = null; - } - } - - @Override public boolean supportsParameter(MethodParameter parameter) { Class paramType = parameter.getParameterType(); @@ -90,7 +74,7 @@ public boolean supportsParameter(MethodParameter parameter) { ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType) || HttpSession.class.isAssignableFrom(paramType) || - (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) || + PushBuilder.class.isAssignableFrom(paramType) || (Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) || InputStream.class.isAssignableFrom(paramType) || Reader.class.isAssignableFrom(paramType) || @@ -143,8 +127,13 @@ private Object resolveArgument(Class paramType, HttpServletRequest request) t } return session; } - else if (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) { - return PushBuilderDelegate.resolvePushBuilder(request, paramType); + else if (PushBuilder.class.isAssignableFrom(paramType)) { + PushBuilder pushBuilder = request.newPushBuilder(); + if (pushBuilder != null && !paramType.isInstance(pushBuilder)) { + throw new IllegalStateException( + "Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder); + } + return pushBuilder; } else if (InputStream.class.isAssignableFrom(paramType)) { InputStream inputStream = request.getInputStream(); @@ -189,22 +178,4 @@ else if (ZoneId.class == paramType) { throw new UnsupportedOperationException("Unknown parameter type: " + paramType.getName()); } - - /** - * Inner class to avoid a hard dependency on Servlet API 4.0 at runtime. - */ - private static class PushBuilderDelegate { - - @Nullable - public static Object resolvePushBuilder(HttpServletRequest request, Class paramType) { - PushBuilder pushBuilder = request.newPushBuilder(); - if (pushBuilder != null && !paramType.isInstance(pushBuilder)) { - throw new IllegalStateException( - "Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder); - } - return pushBuilder; - - } - } - } From b598ad3f331a8a6868c4ee8628e25588d2f35cb1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 19:26:11 +0100 Subject: [PATCH 113/261] Polishing --- .../support/AbstractApplicationContext.java | 41 ++++++++++--------- ...ocalContainerEntityManagerFactoryBean.java | 13 +++--- .../jpa/LocalEntityManagerFactoryBean.java | 26 ++++-------- .../mock/web/MockHttpServletResponse.java | 12 +++++- .../servlet/MockHttpServletResponse.java | 12 +++++- 5 files changed, 58 insertions(+), 46 deletions(-) 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 548c752390a0..40fb85104ebf 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 @@ -135,17 +135,6 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { - /** - * The name of the {@link LifecycleProcessor} bean in the context. - * If none is supplied, a {@link DefaultLifecycleProcessor} is used. - * @since 3.0 - * @see org.springframework.context.LifecycleProcessor - * @see org.springframework.context.support.DefaultLifecycleProcessor - * @see #start() - * @see #stop() - */ - public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; - /** * The name of the {@link MessageSource} bean in the context. * If none is supplied, message resolution is delegated to the parent. @@ -166,6 +155,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader */ public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster"; + /** + * The name of the {@link LifecycleProcessor} bean in the context. + * If none is supplied, a {@link DefaultLifecycleProcessor} is used. + * @since 3.0 + * @see org.springframework.context.LifecycleProcessor + * @see org.springframework.context.support.DefaultLifecycleProcessor + * @see #start() + * @see #stop() + */ + public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor"; + static { // Eagerly load the ContextClosedEvent class to avoid weird classloader issues @@ -796,8 +796,9 @@ protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFa } /** - * Initialize the MessageSource. - * Use parent's if none defined in this context. + * Initialize the {@link MessageSource}. + *

    Uses parent's {@code MessageSource} if none defined in this context. + * @see #MESSAGE_SOURCE_BEAN_NAME */ protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); @@ -827,8 +828,9 @@ protected void initMessageSource() { } /** - * Initialize the ApplicationEventMulticaster. - * Uses SimpleApplicationEventMulticaster if none defined in the context. + * Initialize the {@link ApplicationEventMulticaster}. + *

    Uses {@link SimpleApplicationEventMulticaster} if none defined in the context. + * @see #APPLICATION_EVENT_MULTICASTER_BEAN_NAME * @see org.springframework.context.event.SimpleApplicationEventMulticaster */ protected void initApplicationEventMulticaster() { @@ -851,15 +853,16 @@ protected void initApplicationEventMulticaster() { } /** - * Initialize the LifecycleProcessor. - * Uses DefaultLifecycleProcessor if none defined in the context. + * Initialize the {@link LifecycleProcessor}. + *

    Uses {@link DefaultLifecycleProcessor} if none defined in the context. + * @since 3.0 + * @see #LIFECYCLE_PROCESSOR_BEAN_NAME * @see org.springframework.context.support.DefaultLifecycleProcessor */ protected void initLifecycleProcessor() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) { - this.lifecycleProcessor = - beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); + this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class); if (logger.isTraceEnabled()) { logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]"); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java index f977c277bd1d..6749fe7fc183 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -264,8 +264,9 @@ public void setValidationMode(ValidationMode validationMode) { * @see jakarta.persistence.spi.PersistenceUnitInfo#getNonJtaDataSource() * @see #setPersistenceUnitManager */ - public void setDataSource(DataSource dataSource) { - this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(dataSource)); + public void setDataSource(@Nullable DataSource dataSource) { + this.internalPersistenceUnitManager.setDataSourceLookup( + dataSource != null ? new SingleDataSourceLookup(dataSource) : null); this.internalPersistenceUnitManager.setDefaultDataSource(dataSource); } @@ -281,8 +282,9 @@ public void setDataSource(DataSource dataSource) { * @see jakarta.persistence.spi.PersistenceUnitInfo#getJtaDataSource() * @see #setPersistenceUnitManager */ - public void setJtaDataSource(DataSource jtaDataSource) { - this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(jtaDataSource)); + public void setJtaDataSource(@Nullable DataSource jtaDataSource) { + this.internalPersistenceUnitManager.setDataSourceLookup( + jtaDataSource != null ? new SingleDataSourceLookup(jtaDataSource) : null); this.internalPersistenceUnitManager.setDefaultJtaDataSource(jtaDataSource); } @@ -427,6 +429,7 @@ public String getPersistenceUnitName() { } @Override + @Nullable public DataSource getDataSource() { if (this.persistenceUnitInfo != null) { return (this.persistenceUnitInfo.getJtaDataSource() != null ? diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java index 68e740057854..1aaeb9a715f0 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -28,28 +28,18 @@ * shared JPA EntityManagerFactory in a Spring application context; the * EntityManagerFactory can then be passed to JPA-based DAOs via * dependency injection. Note that switching to a JNDI lookup or to a - * {@link LocalContainerEntityManagerFactoryBean} - * definition is just a matter of configuration! + * {@link LocalContainerEntityManagerFactoryBean} definition based on the + * JPA container contract is just a matter of configuration! * *

    Configuration settings are usually read from a {@code META-INF/persistence.xml} * config file, residing in the class path, according to the JPA standalone bootstrap - * contract. Additionally, most JPA providers will require a special VM agent - * (specified on JVM startup) that allows them to instrument application classes. - * See the Java Persistence API specification and your provider documentation - * for setup details. - * - *

    This EntityManagerFactory bootstrap is appropriate for standalone applications - * which solely use JPA for data access. If you want to set up your persistence - * provider for an external DataSource and/or for global transactions which span - * multiple resources, you will need to either deploy it into a full Jakarta EE - * application server and access the deployed EntityManagerFactory via JNDI, - * or use Spring's {@link LocalContainerEntityManagerFactoryBean} with appropriate - * configuration for local setup according to JPA's container contract. + * contract. See the Java Persistence API specification and your persistence provider + * documentation for setup details. Additionally, JPA properties can also be added + * on this FactoryBean via {@link #setJpaProperties}/{@link #setJpaPropertyMap}. * *

    Note: This FactoryBean has limited configuration power in terms of - * what configuration it is able to pass to the JPA provider. If you need more - * flexible configuration, for example passing a Spring-managed JDBC DataSource - * to the JPA provider, consider using Spring's more powerful + * the configuration that it is able to pass to the JPA provider. If you need + * more flexible configuration options, consider using Spring's more powerful * {@link LocalContainerEntityManagerFactoryBean} instead. * * @author Juergen Hoeller diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index 392efe675b2f..41c1461f7fdb 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -292,6 +292,11 @@ public void setContentLength(int contentLength) { doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); } + /** + * Get the length of the content body from the HTTP Content-Length header. + * @return the value of the Content-Length header + * @see #setContentLength(int) + */ public int getContentLength() { return (int) this.contentLength; } @@ -742,7 +747,7 @@ private void setCookie(Cookie cookie) { @Override public void setStatus(int status) { - if (!this.isCommitted()) { + if (!isCommitted()) { this.status = status; } } @@ -752,6 +757,9 @@ public int getStatus() { return this.status; } + /** + * Return the error message used when calling {@link HttpServletResponse#sendError(int, String)}. + */ @Nullable public String getErrorMessage() { return this.errorMessage; diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java index 0c06cb8c38d8..5313200c1b05 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -292,6 +292,11 @@ public void setContentLength(int contentLength) { doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); } + /** + * Get the length of the content body from the HTTP Content-Length header. + * @return the value of the Content-Length header + * @see #setContentLength(int) + */ public int getContentLength() { return (int) this.contentLength; } @@ -742,7 +747,7 @@ private void setCookie(Cookie cookie) { @Override public void setStatus(int status) { - if (!this.isCommitted()) { + if (!isCommitted()) { this.status = status; } } @@ -752,6 +757,9 @@ public int getStatus() { return this.status; } + /** + * Return the error message used when calling {@link HttpServletResponse#sendError(int, String)}. + */ @Nullable public String getErrorMessage() { return this.errorMessage; From d57775bbb2d7b1cbad0eaf3897075f20b25132ec Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 21:38:38 +0100 Subject: [PATCH 114/261] Polishing --- .../web/context/request/async/WebAsyncManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index a94fbd3900ec..91664d306a2c 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -457,8 +457,8 @@ public void startDeferredResultProcessing( } }); - this.asyncWebRequest.addCompletionHandler(() - -> interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult)); + this.asyncWebRequest.addCompletionHandler(() -> + interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult)); interceptorChain.applyBeforeConcurrentHandling(this.asyncWebRequest, deferredResult); startAsyncProcessing(processingContext); From 5c34e1d11a130b856cba8df68bad816756498b02 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Feb 2024 21:38:57 +0100 Subject: [PATCH 115/261] Upgrade to Undertow 2.3.12, OpenPDF 1.3.41, JRuby 9.4.6 --- framework-platform/framework-platform.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 771346b10e53..f8a7623050ee 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -25,7 +25,7 @@ dependencies { api("com.fasterxml:aalto-xml:1.3.2") api("com.fasterxml.woodstox:woodstox-core:6.5.1") api("com.github.ben-manes.caffeine:caffeine:3.1.8") - api("com.github.librepdf:openpdf:1.3.40") + api("com.github.librepdf:openpdf:1.3.41") api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:jsr305:3.0.2") api("com.google.code.gson:gson:2.10.1") @@ -54,9 +54,9 @@ dependencies { api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") api("io.reactivex.rxjava3:rxjava:3.1.8") api("io.smallrye.reactive:mutiny:1.10.0") - api("io.undertow:undertow-core:2.3.11.Final") - api("io.undertow:undertow-servlet:2.3.11.Final") - api("io.undertow:undertow-websockets-jsr:2.3.11.Final") + api("io.undertow:undertow-core:2.3.12.Final") + api("io.undertow:undertow-servlet:2.3.12.Final") + api("io.undertow:undertow-websockets-jsr:2.3.12.Final") api("io.vavr:vavr:0.10.4") api("jakarta.activation:jakarta.activation-api:2.0.1") api("jakarta.annotation:jakarta.annotation-api:2.0.0") @@ -126,7 +126,7 @@ dependencies { api("org.hibernate:hibernate-validator:7.0.5.Final") api("org.hsqldb:hsqldb:2.7.2") api("org.javamoney:moneta:1.4.2") - api("org.jruby:jruby:9.4.5.0") + api("org.jruby:jruby:9.4.6.0") api("org.junit.support:testng-engine:1.0.4") api("org.mozilla:rhino:1.7.14") api("org.ogce:xpp3:1.1.6") From 814c003b43f9577dce79702a95f20157bd81fc09 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 29 Feb 2024 17:46:18 +0000 Subject: [PATCH 116/261] Align 5.3.x with 6.1.x In preparation for a larger update, start by aligning with 6.1.x, which includes changes for gh-32042 and gh-30232. See gh-32341 --- .../request/async/WebAsyncManager.java | 22 ++++- .../web/util/DisconnectedClientHelper.java | 95 +++++++++++++++++++ 2 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 91664d306a2c..16875352fda1 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -22,7 +22,6 @@ import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionException; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -35,6 +34,7 @@ import org.springframework.util.Assert; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler; +import org.springframework.web.util.DisconnectedClientHelper; /** * The central class for managing asynchronous request processing, mainly intended @@ -68,6 +68,16 @@ public final class WebAsyncManager { private static final Log logger = LogFactory.getLog(WebAsyncManager.class); + /** + * Log category to use for network failure after a client has gone away. + * @see DisconnectedClientHelper + */ + private static final String DISCONNECTED_CLIENT_LOG_CATEGORY = + "org.springframework.web.server.DisconnectedClient"; + + private static final DisconnectedClientHelper disconnectedClientHelper = + new DisconnectedClientHelper(DISCONNECTED_CLIENT_LOG_CATEGORY); + private static final CallableProcessingInterceptor timeoutCallableInterceptor = new TimeoutCallableProcessingInterceptor(); @@ -350,10 +360,9 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. }); interceptorChain.setTaskFuture(future); } - catch (RejectedExecutionException ex) { + catch (Throwable ex) { Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex); setConcurrentResultAndDispatch(result); - throw ex; } } @@ -394,9 +403,12 @@ private void setConcurrentResultAndDispatch(@Nullable Object result) { return; } + if (result instanceof Exception ex && disconnectedClientHelper.checkAndLogClientDisconnectedException(ex)) { + return; + } + if (logger.isDebugEnabled()) { - boolean isError = result instanceof Throwable; - logger.debug("Async " + (isError ? "error" : "result set") + + logger.debug("Async " + (this.errorHandlingInProgress ? "error" : "result set") + ", dispatch to " + formatUri(this.asyncWebRequest)); } this.asyncWebRequest.dispatch(); diff --git a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java new file mode 100644 index 000000000000..4ba3441a4726 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2023 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; + +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.NestedExceptionUtils; +import org.springframework.util.Assert; + +/** + * Utility methods to assist with identifying and logging exceptions that indicate + * the client has gone away. Such exceptions fill logs with unnecessary stack + * traces. The utility methods help to log a single line message at DEBUG level, + * and a full stacktrace at TRACE level. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +public class DisconnectedClientHelper { + + private static final Set EXCEPTION_PHRASES = + Set.of("broken pipe", "connection reset by peer"); + + private static final Set EXCEPTION_TYPE_NAMES = + Set.of("AbortedException", "ClientAbortException", "EOFException", "EofException"); + + private final Log logger; + + + public DisconnectedClientHelper(String logCategory) { + Assert.notNull(logCategory, "'logCategory' is required"); + this.logger = LogFactory.getLog(logCategory); + } + + + /** + * Check via {@link #isClientDisconnectedException} if the exception + * indicates the remote client disconnected, and if so log a single line + * message when DEBUG is on, and a full stacktrace when TRACE is on for + * the configured logger. + */ + public boolean checkAndLogClientDisconnectedException(Throwable ex) { + if (isClientDisconnectedException(ex)) { + if (logger.isTraceEnabled()) { + logger.trace("Looks like the client has gone away", ex); + } + else if (logger.isDebugEnabled()) { + logger.debug("Looks like the client has gone away: " + ex + + " (For a full stack trace, set the log category '" + logger + "' to TRACE level.)"); + } + return true; + } + return false; + } + + /** + * Whether the given exception indicates the client has gone away. + * Known cases covered: + *

      + *
    • ClientAbortException or EOFException for Tomcat + *
    • EofException for Jetty + *
    • IOException "Broken pipe" or "connection reset by peer" + *
    + */ + public static boolean isClientDisconnectedException(Throwable ex) { + String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage(); + if (message != null) { + String text = message.toLowerCase(); + for (String phrase : EXCEPTION_PHRASES) { + if (text.contains(phrase)) { + return true; + } + } + } + return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName()); + } + +} From b208c63414fe554f6523bf470aaa1cb161f9e7c7 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 1 Mar 2024 22:31:09 +0000 Subject: [PATCH 117/261] Add state and response wrapping to StandardServletAsyncWebRequest The wrapped response prevents use after AsyncListener onError or completion to ensure compliance with Servlet Spec 2.3.3.4. The wrapped response is applied in RequestMappingHandlerAdapter. The wrapped response raises AsyncRequestNotUsableException that is now handled in DefaultHandlerExceptionResolver. See gh-32341 --- .../async/AsyncRequestNotUsableException.java | 44 ++ .../async/StandardServletAsyncWebRequest.java | 526 +++++++++++++++++- .../request/async/WebAsyncManager.java | 9 + .../context/request/async/WebAsyncUtils.java | 7 +- .../StandardServletAsyncWebRequestTests.java | 7 +- .../RequestMappingHandlerAdapter.java | 25 +- .../DefaultHandlerExceptionResolver.java | 27 + .../ResponseEntityExceptionHandlerTests.java | 6 +- 8 files changed, 621 insertions(+), 30 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java new file mode 100644 index 000000000000..45198fe728d6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestNotUsableException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2024 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.context.request.async; + +import java.io.IOException; + +/** + * Raised when the response for an asynchronous request becomes unusable as + * indicated by a write failure, or a Servlet container error notification, or + * after the async request has completed. + * + *

    The exception relies on response wrapping, and on {@code AsyncListener} + * notifications, managed by {@link StandardServletAsyncWebRequest}. + * + * @author Rossen Stoyanchev + * @since 5.3.33 + */ +@SuppressWarnings("serial") +public class AsyncRequestNotUsableException extends IOException { + + + public AsyncRequestNotUsableException(String message) { + super(message); + } + + public AsyncRequestNotUsableException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index eb46ccb64790..aa48427b2ad6 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,16 +17,22 @@ package org.springframework.web.context.request.async; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Locale; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncEvent; import jakarta.servlet.AsyncListener; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -45,8 +51,6 @@ */ public class StandardServletAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest, AsyncListener { - private final AtomicBoolean asyncCompleted = new AtomicBoolean(); - private final List timeoutHandlers = new ArrayList<>(); private final List> exceptionHandlers = new ArrayList<>(); @@ -59,6 +63,10 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements @Nullable private AsyncContext asyncContext; + private State state; + + private final ReentrantLock stateLock = new ReentrantLock(); + /** * Create a new instance for the given request/response pair. @@ -66,7 +74,26 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements * @param response current HTTP response */ public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { - super(request, response); + this(request, response, null); + } + + /** + * Constructor to wrap the request and response for the current dispatch that + * also picks up the state of the last (probably the REQUEST) dispatch. + * @param request current HTTP request + * @param response current HTTP response + * @param previousRequest the existing request from the last dispatch + * @since 5.3.33 + */ + StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response, + @Nullable StandardServletAsyncWebRequest previousRequest) { + + super(request, new LifecycleHttpServletResponse(response)); + + this.state = (previousRequest != null ? previousRequest.state : State.NEW); + + //noinspection DataFlowIssue + ((LifecycleHttpServletResponse) getResponse()).setAsyncWebRequest(this); } @@ -107,7 +134,7 @@ public boolean isAsyncStarted() { */ @Override public boolean isAsyncComplete() { - return this.asyncCompleted.get(); + return (this.state == State.COMPLETED); } @Override @@ -117,11 +144,18 @@ public void startAsync() { "in async request processing. This is done in Java code using the Servlet API " + "or by adding \"true\" to servlet and " + "filter declarations in web.xml."); - Assert.state(!isAsyncComplete(), "Async processing has already completed"); if (isAsyncStarted()) { return; } + + if (this.state == State.NEW) { + this.state = State.ASYNC; + } + else { + Assert.state(this.state == State.ASYNC, "Cannot start async: [" + this.state + "]"); + } + this.asyncContext = getRequest().startAsync(getRequest(), getResponse()); this.asyncContext.addListener(this); if (this.timeout != null) { @@ -131,8 +165,10 @@ public void startAsync() { @Override public void dispatch() { - Assert.state(this.asyncContext != null, "Cannot dispatch without an AsyncContext"); - this.asyncContext.dispatch(); + Assert.state(this.asyncContext != null, "AsyncContext not yet initialized"); + if (!this.isAsyncComplete()) { + this.asyncContext.dispatch(); + } } @@ -151,14 +187,478 @@ public void onTimeout(AsyncEvent event) throws IOException { @Override public void onError(AsyncEvent event) throws IOException { - this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable())); + this.stateLock.lock(); + try { + transitionToErrorState(); + Throwable ex = event.getThrowable(); + this.exceptionHandlers.forEach(consumer -> consumer.accept(ex)); + } + finally { + this.stateLock.unlock(); + } + } + + private void transitionToErrorState() { + if (!isAsyncComplete()) { + this.state = State.ERROR; + } } @Override public void onComplete(AsyncEvent event) throws IOException { - this.completionHandlers.forEach(Runnable::run); - this.asyncContext = null; - this.asyncCompleted.set(true); + this.stateLock.lock(); + try { + this.completionHandlers.forEach(Runnable::run); + this.asyncContext = null; + this.state = State.COMPLETED; + } + finally { + this.stateLock.unlock(); + } + } + + + /** + * Response wrapper to wrap the output stream with {@link LifecycleServletOutputStream}. + */ + private static final class LifecycleHttpServletResponse extends HttpServletResponseWrapper { + + @Nullable + private StandardServletAsyncWebRequest asyncWebRequest; + + @Nullable + private ServletOutputStream outputStream; + + @Nullable + private PrintWriter writer; + + public LifecycleHttpServletResponse(HttpServletResponse response) { + super(response); + } + + public void setAsyncWebRequest(StandardServletAsyncWebRequest asyncWebRequest) { + this.asyncWebRequest = asyncWebRequest; + } + + @Override + public ServletOutputStream getOutputStream() { + if (this.outputStream == null) { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + this.outputStream = new LifecycleServletOutputStream( + (HttpServletResponse) getResponse(), this.asyncWebRequest); + } + return this.outputStream; + } + + @Override + public PrintWriter getWriter() throws IOException { + if (this.writer == null) { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + this.writer = new LifecyclePrintWriter(getResponse().getWriter(), this.asyncWebRequest); + } + return this.writer; + } + } + + + /** + * Wraps a ServletOutputStream to prevent use after Servlet container onError + * notifications, and after async request completion. + */ + private static final class LifecycleServletOutputStream extends ServletOutputStream { + + private final HttpServletResponse delegate; + + private final StandardServletAsyncWebRequest asyncWebRequest; + + private LifecycleServletOutputStream( + HttpServletResponse delegate, StandardServletAsyncWebRequest asyncWebRequest) { + + this.delegate = delegate; + this.asyncWebRequest = asyncWebRequest; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + throw new UnsupportedOperationException(); + } + + @Override + public void write(int b) throws IOException { + obtainLockAndCheckState(); + try { + this.delegate.getOutputStream().write(b); + } + catch (IOException ex) { + handleIOException(ex, "ServletOutputStream failed to write"); + } + finally { + releaseLock(); + } + } + + public void write(byte[] buf, int offset, int len) throws IOException { + obtainLockAndCheckState(); + try { + this.delegate.getOutputStream().write(buf, offset, len); + } + catch (IOException ex) { + handleIOException(ex, "ServletOutputStream failed to write"); + } + finally { + releaseLock(); + } + } + + @Override + public void flush() throws IOException { + obtainLockAndCheckState(); + try { + this.delegate.getOutputStream().flush(); + } + catch (IOException ex) { + handleIOException(ex, "ServletOutputStream failed to flush"); + } + finally { + releaseLock(); + } + } + + @Override + public void close() throws IOException { + obtainLockAndCheckState(); + try { + this.delegate.getOutputStream().close(); + } + catch (IOException ex) { + handleIOException(ex, "ServletOutputStream failed to close"); + } + finally { + releaseLock(); + } + } + + private void obtainLockAndCheckState() throws AsyncRequestNotUsableException { + if (state() != State.NEW) { + stateLock().lock(); + if (state() != State.ASYNC) { + stateLock().unlock(); + throw new AsyncRequestNotUsableException("Response not usable after " + + (state() == State.COMPLETED ? + "async request completion" : "onError notification") + "."); + } + } + } + + private void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException { + this.asyncWebRequest.transitionToErrorState(); + throw new AsyncRequestNotUsableException(msg, ex); + } + + private void releaseLock() { + if (state() != State.NEW) { + stateLock().unlock(); + } + } + + private State state() { + return this.asyncWebRequest.state; + } + + private Lock stateLock() { + return this.asyncWebRequest.stateLock; + } + + } + + + /** + * Wraps a PrintWriter to prevent use after Servlet container onError + * notifications, and after async request completion. + */ + private static final class LifecyclePrintWriter extends PrintWriter { + + private final PrintWriter delegate; + + private final StandardServletAsyncWebRequest asyncWebRequest; + + private LifecyclePrintWriter(PrintWriter delegate, StandardServletAsyncWebRequest asyncWebRequest) { + super(delegate); + this.delegate = delegate; + this.asyncWebRequest = asyncWebRequest; + } + + @Override + public void flush() { + if (tryObtainLockAndCheckState()) { + try { + this.delegate.flush(); + } + finally { + releaseLock(); + } + } + } + + @Override + public void close() { + if (tryObtainLockAndCheckState()) { + try { + this.delegate.close(); + } + finally { + releaseLock(); + } + } + } + + @Override + public boolean checkError() { + return this.delegate.checkError(); + } + + @Override + public void write(int c) { + if (tryObtainLockAndCheckState()) { + try { + this.delegate.write(c); + } + finally { + releaseLock(); + } + } + } + + @Override + public void write(char[] buf, int off, int len) { + if (tryObtainLockAndCheckState()) { + try { + this.delegate.write(buf, off, len); + } + finally { + releaseLock(); + } + } + } + + @Override + public void write(char[] buf) { + this.delegate.write(buf); + } + + @Override + public void write(String s, int off, int len) { + if (tryObtainLockAndCheckState()) { + try { + this.delegate.write(s, off, len); + } + finally { + releaseLock(); + } + } + } + + @Override + public void write(String s) { + this.delegate.write(s); + } + + private boolean tryObtainLockAndCheckState() { + if (state() == State.NEW) { + return true; + } + if (stateLock().tryLock()) { + if (state() == State.ASYNC) { + return true; + } + stateLock().unlock(); + } + return false; + } + + private void releaseLock() { + if (state() != State.NEW) { + stateLock().unlock(); + } + } + + private State state() { + return this.asyncWebRequest.state; + } + + private Lock stateLock() { + return this.asyncWebRequest.stateLock; + } + + // Plain delegates + + @Override + public void print(boolean b) { + this.delegate.print(b); + } + + @Override + public void print(char c) { + this.delegate.print(c); + } + + @Override + public void print(int i) { + this.delegate.print(i); + } + + @Override + public void print(long l) { + this.delegate.print(l); + } + + @Override + public void print(float f) { + this.delegate.print(f); + } + + @Override + public void print(double d) { + this.delegate.print(d); + } + + @Override + public void print(char[] s) { + this.delegate.print(s); + } + + @Override + public void print(String s) { + this.delegate.print(s); + } + + @Override + public void print(Object obj) { + this.delegate.print(obj); + } + + @Override + public void println() { + this.delegate.println(); + } + + @Override + public void println(boolean x) { + this.delegate.println(x); + } + + @Override + public void println(char x) { + this.delegate.println(x); + } + + @Override + public void println(int x) { + this.delegate.println(x); + } + + @Override + public void println(long x) { + this.delegate.println(x); + } + + @Override + public void println(float x) { + this.delegate.println(x); + } + + @Override + public void println(double x) { + this.delegate.println(x); + } + + @Override + public void println(char[] x) { + this.delegate.println(x); + } + + @Override + public void println(String x) { + this.delegate.println(x); + } + + @Override + public void println(Object x) { + this.delegate.println(x); + } + + @Override + public PrintWriter printf(String format, Object... args) { + return this.delegate.printf(format, args); + } + + @Override + public PrintWriter printf(Locale l, String format, Object... args) { + return this.delegate.printf(l, format, args); + } + + @Override + public PrintWriter format(String format, Object... args) { + return this.delegate.format(format, args); + } + + @Override + public PrintWriter format(Locale l, String format, Object... args) { + return this.delegate.format(l, format, args); + } + + @Override + public PrintWriter append(CharSequence csq) { + return this.delegate.append(csq); + } + + @Override + public PrintWriter append(CharSequence csq, int start, int end) { + return this.delegate.append(csq, start, end); + } + + @Override + public PrintWriter append(char c) { + return this.delegate.append(c); + } + } + + + /** + * Represents a state for {@link StandardServletAsyncWebRequest} to be in. + *

    +	 *        NEW
    +	 *         |
    +	 *         v
    +	 *       ASYNC----> +
    +	 *         |        |
    +	 *         v        |
    +	 *       ERROR      |
    +	 *         |        |
    +	 *         v        |
    +	 *     COMPLETED <--+
    +	 * 
    + * @since 5.3.33 + */ + private enum State { + + /** New request (thas may not do async handling). */ + NEW, + + /** Async handling has started. */ + ASYNC, + + /** onError notification received, or ServletOutputStream failed. */ + ERROR, + + /** onComplete notification received. */ + COMPLETED + } } diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 16875352fda1..8f98dc9279ed 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -135,6 +135,15 @@ public void setAsyncWebRequest(AsyncWebRequest asyncWebRequest) { WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST)); } + /** + * Return the current {@link AsyncWebRequest}. + * @since 5.3.33 + */ + @Nullable + public AsyncWebRequest getAsyncWebRequest() { + return this.asyncWebRequest; + } + /** * Configure an AsyncTaskExecutor for use with concurrent processing via * {@link #startCallableProcessing(Callable, Object...)}. diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java index 7c948f1037f2..4d089061f6cc 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,7 +82,10 @@ public static WebAsyncManager getAsyncManager(WebRequest webRequest) { * @return an AsyncWebRequest instance (never {@code null}) */ public static AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) { - return new StandardServletAsyncWebRequest(request, response); + AsyncWebRequest prev = getAsyncManager(request).getAsyncWebRequest(); + return (prev instanceof StandardServletAsyncWebRequest standardRequest ? + new StandardServletAsyncWebRequest(request, response, standardRequest) : + new StandardServletAsyncWebRequest(request, response)); } } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java index b80150d815df..011c5a33dcff 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -95,9 +95,8 @@ public void startAsyncNotSupported() throws Exception { @Test public void startAsyncAfterCompleted() throws Exception { this.asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(this.request, this.response))); - assertThatIllegalStateException().isThrownBy( - this.asyncRequest::startAsync) - .withMessage("Async processing has already completed"); + assertThatIllegalStateException().isThrownBy(this.asyncRequest::startAsync) + .withMessage("Cannot start async: [COMPLETED]"); } @Test 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 28dbf3243ba5..068c1d04da46 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 @@ -843,7 +843,21 @@ private SessionAttributesHandler getSessionAttributesHandler(HandlerMethod handl protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { - ServletWebRequest webRequest = new ServletWebRequest(request, response); + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); + asyncWebRequest.setTimeout(this.asyncRequestTimeout); + + asyncManager.setTaskExecutor(this.taskExecutor); + asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.registerCallableInterceptors(this.callableInterceptors); + asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); + + // Obtain wrapped response to enforce lifecycle rule from Servlet spec, section 2.3.3.4 + response = asyncWebRequest.getNativeResponse(HttpServletResponse.class); + + ServletWebRequest webRequest = (asyncWebRequest instanceof ServletWebRequest ? + (ServletWebRequest) asyncWebRequest : new ServletWebRequest(request, response)); + WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); @@ -862,15 +876,6 @@ protected ModelAndView invokeHandlerMethod(HttpServletRequest request, modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); - AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); - asyncWebRequest.setTimeout(this.asyncRequestTimeout); - - WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); - asyncManager.setTaskExecutor(this.taskExecutor); - asyncManager.setAsyncWebRequest(asyncWebRequest); - asyncManager.registerCallableInterceptors(this.callableInterceptors); - asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); - if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); Object[] resultContext = asyncManager.getConcurrentResultContext(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index ce6d4aacdc52..d39584fd2f11 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -44,6 +44,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.MissingServletRequestPartException; @@ -129,6 +130,10 @@ *
    AsyncRequestTimeoutException
    *
    503 (SC_SERVICE_UNAVAILABLE)
    * + * + *
    AsyncRequestNotUsableException
    + *
    Not applicable
    + * * * * @@ -223,6 +228,10 @@ else if (ex instanceof HttpMessageNotWritableException theEx) { else if (ex instanceof BindException theEx) { return handleBindException(theEx, request, response, handler); } + else if (ex instanceof AsyncRequestNotUsableException) { + return handleAsyncRequestNotUsableException( + (AsyncRequestNotUsableException) ex, request, response, handler); + } } catch (Exception handlerEx) { if (logger.isWarnEnabled()) { @@ -434,6 +443,24 @@ protected ModelAndView handleAsyncRequestTimeoutException(AsyncRequestTimeoutExc return null; } + /** + * Handle the case of an I/O failure from the ServletOutputStream. + *

    By default, do nothing since the response is not usable. + * @param ex the {@link AsyncRequestTimeoutException} to be handled + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler, or {@code null} if none chosen + * at the time of the exception (for example, if multipart resolution failed) + * @return an empty ModelAndView indicating the exception was handled + * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} + * @since 5.3.33 + */ + protected ModelAndView handleAsyncRequestNotUsableException(AsyncRequestNotUsableException ex, + HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) { + + return new ModelAndView(); + } + /** * Handle an {@link ErrorResponse} exception. *

    The default implementation sets status and the headers of the response diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index d30867ccab72..e7a47c80509e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -97,6 +97,10 @@ public void supportsAllDefaultHandlerExceptionResolverExceptionTypes() throws Ex .filter(method -> method.getName().startsWith("handle") && (method.getParameterCount() == 4)) .filter(method -> !method.getName().equals("handleErrorResponse")) .map(method -> method.getParameterTypes()[0]) + .filter(exceptionType -> { + String name = exceptionType.getSimpleName(); + return !name.equals("AsyncRequestNotUsableException"); + }) .forEach(exceptionType -> assertThat(annotation.value()) .as("@ExceptionHandler is missing declaration for " + exceptionType.getName()) .contains((Class) exceptionType)); From 1a5661d4263c1e0ab23cff7532faaa6dc33e42b3 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 1 Mar 2024 22:39:22 +0000 Subject: [PATCH 118/261] Improve concurrent handling of result in WebAsyncManager 1. Use state transitions 2. Increase synchronized scope in setConcurrentResultAndDispatch See gh-32341 --- .../request/async/WebAsyncManager.java | 138 +++++++++++------- 1 file changed, 87 insertions(+), 51 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 8f98dc9279ed..5ff3485a670d 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; @@ -34,7 +35,6 @@ import org.springframework.util.Assert; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.async.DeferredResult.DeferredResultHandler; -import org.springframework.web.util.DisconnectedClientHelper; /** * The central class for managing asynchronous request processing, mainly intended @@ -68,16 +68,6 @@ public final class WebAsyncManager { private static final Log logger = LogFactory.getLog(WebAsyncManager.class); - /** - * Log category to use for network failure after a client has gone away. - * @see DisconnectedClientHelper - */ - private static final String DISCONNECTED_CLIENT_LOG_CATEGORY = - "org.springframework.web.server.DisconnectedClient"; - - private static final DisconnectedClientHelper disconnectedClientHelper = - new DisconnectedClientHelper(DISCONNECTED_CLIENT_LOG_CATEGORY); - private static final CallableProcessingInterceptor timeoutCallableInterceptor = new TimeoutCallableProcessingInterceptor(); @@ -98,12 +88,7 @@ public final class WebAsyncManager { @Nullable private volatile Object[] concurrentResultContext; - /* - * Whether the concurrentResult is an error. If such errors remain unhandled, some - * Servlet containers will call AsyncListener#onError at the end, after the ASYNC - * and/or the ERROR dispatch (Boot's case), and we need to ignore those. - */ - private volatile boolean errorHandlingInProgress; + private final AtomicReference state = new AtomicReference<>(State.NOT_STARTED); private final Map callableInterceptors = new LinkedHashMap<>(); @@ -265,6 +250,12 @@ public void registerDeferredResultInterceptors(DeferredResultProcessingIntercept * {@linkplain #getConcurrentResultContext() concurrentResultContext}. */ public void clearConcurrentResult() { + if (!this.state.compareAndSet(State.RESULT_SET, State.NOT_STARTED)) { + if (logger.isDebugEnabled()) { + logger.debug("Unexpected call to clear: [" + this.state.get() + "]"); + } + return; + } synchronized (WebAsyncManager.this) { this.concurrentResult = RESULT_NONE; this.concurrentResultContext = null; @@ -305,6 +296,11 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. Assert.notNull(webAsyncTask, "WebAsyncTask must not be null"); Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); + if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) { + throw new IllegalStateException( + "Unexpected call to startCallableProcessing: [" + this.state.get() + "]"); + } + Long timeout = webAsyncTask.getTimeout(); if (timeout != null) { this.asyncWebRequest.setTimeout(timeout); @@ -328,7 +324,7 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. this.asyncWebRequest.addTimeoutHandler(() -> { if (logger.isDebugEnabled()) { - logger.debug("Async request timeout for " + formatUri(this.asyncWebRequest)); + logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest)); } Object result = interceptorChain.triggerAfterTimeout(this.asyncWebRequest, callable); if (result != CallableProcessingInterceptor.RESULT_NONE) { @@ -337,14 +333,12 @@ public void startCallableProcessing(final WebAsyncTask webAsyncTask, Object.. }); this.asyncWebRequest.addErrorHandler(ex -> { - if (!this.errorHandlingInProgress) { - if (logger.isDebugEnabled()) { - logger.debug("Async request error for " + formatUri(this.asyncWebRequest) + ": " + ex); - } - Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex); - result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex); - setConcurrentResultAndDispatch(result); + if (logger.isDebugEnabled()) { + logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest) + ": " + ex); } + Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex); + result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex); + setConcurrentResultAndDispatch(result); }); this.asyncWebRequest.addCompletionHandler(() -> @@ -396,31 +390,34 @@ private void logExecutorWarning(AsyncWebRequest asyncWebRequest) { } private void setConcurrentResultAndDispatch(@Nullable Object result) { + Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); synchronized (WebAsyncManager.this) { - if (this.concurrentResult != RESULT_NONE) { + if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) { + if (logger.isDebugEnabled()) { + logger.debug("Async result already set: " + + "[" + this.state.get() + "], ignored result: " + result + + " for " + formatUri(this.asyncWebRequest)); + } return; } - this.concurrentResult = result; - this.errorHandlingInProgress = (result instanceof Throwable); - } - Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); - if (this.asyncWebRequest.isAsyncComplete()) { + this.concurrentResult = result; if (logger.isDebugEnabled()) { - logger.debug("Async result set but request already complete: " + formatUri(this.asyncWebRequest)); + logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest)); } - return; - } - if (result instanceof Exception ex && disconnectedClientHelper.checkAndLogClientDisconnectedException(ex)) { - return; - } + if (this.asyncWebRequest.isAsyncComplete()) { + if (logger.isDebugEnabled()) { + logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest)); + } + return; + } - if (logger.isDebugEnabled()) { - logger.debug("Async " + (this.errorHandlingInProgress ? "error" : "result set") + - ", dispatch to " + formatUri(this.asyncWebRequest)); + if (logger.isDebugEnabled()) { + logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest)); + } + this.asyncWebRequest.dispatch(); } - this.asyncWebRequest.dispatch(); } /** @@ -443,6 +440,11 @@ public void startDeferredResultProcessing( Assert.notNull(deferredResult, "DeferredResult must not be null"); Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); + if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) { + throw new IllegalStateException( + "Unexpected call to startDeferredResultProcessing: [" + this.state.get() + "]"); + } + Long timeout = deferredResult.getTimeoutValue(); if (timeout != null) { this.asyncWebRequest.setTimeout(timeout); @@ -456,6 +458,9 @@ public void startDeferredResultProcessing( final DeferredResultInterceptorChain interceptorChain = new DeferredResultInterceptorChain(interceptors); this.asyncWebRequest.addTimeoutHandler(() -> { + if (logger.isDebugEnabled()) { + logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest)); + } try { interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult); } @@ -465,16 +470,17 @@ public void startDeferredResultProcessing( }); this.asyncWebRequest.addErrorHandler(ex -> { - if (!this.errorHandlingInProgress) { - try { - if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) { - return; - } - deferredResult.setErrorResult(ex); - } - catch (Throwable interceptorEx) { - setConcurrentResultAndDispatch(interceptorEx); + if (logger.isDebugEnabled()) { + logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest)); + } + try { + if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) { + return; } + deferredResult.setErrorResult(ex); + } + catch (Throwable interceptorEx) { + setConcurrentResultAndDispatch(interceptorEx); } }); @@ -500,10 +506,13 @@ private void startAsyncProcessing(Object[] processingContext) { synchronized (WebAsyncManager.this) { this.concurrentResult = RESULT_NONE; this.concurrentResultContext = processingContext; - this.errorHandlingInProgress = false; } Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null"); + if (logger.isDebugEnabled()) { + logger.debug("Started async request for " + formatUri(this.asyncWebRequest)); + } + this.asyncWebRequest.startAsync(); if (logger.isDebugEnabled()) { logger.debug("Started async request"); @@ -515,4 +524,31 @@ private static String formatUri(AsyncWebRequest asyncWebRequest) { return (request != null ? request.getRequestURI() : "servlet container"); } + + /** + * Represents a state for {@link WebAsyncManager} to be in. + *

    +	 *        NOT_STARTED <------+
    +	 *             |             |
    +	 *             v             |
    +	 *      ASYNC_PROCESSING     |
    +	 *             |             |
    +	 *             v             |
    +	 *         RESULT_SET -------+
    +	 * 
    + * @since 5.3.33 + */ + private enum State { + + /** No async processing in progress. */ + NOT_STARTED, + + /** Async handling has started, but the result hasn't been set yet. */ + ASYNC_PROCESSING, + + /** The result is set, and an async dispatch was performed, unless there is a network error. */ + RESULT_SET + + } + } From 7029042e44096475fef5de1ea4aa4e0c2bcece4e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 4 Mar 2024 22:48:52 +0100 Subject: [PATCH 119/261] Polishing (cherry picked from commit e9110c072980461f17b59456dcb3bcc1fa2bf9a3) --- ...AbstractFallbackJCacheOperationSource.java | 25 +++++---- .../AnnotationJCacheOperationSource.java | 8 ++- ...anFactoryJCacheOperationSourceAdvisor.java | 3 +- .../interceptor/JCacheOperationSource.java | 4 +- .../JCacheOperationSourcePointcut.java | 4 +- .../AbstractFallbackCacheOperationSource.java | 52 +++++++++---------- .../interceptor/CacheOperationSource.java | 4 +- .../CacheOperationSourcePointcut.java | 14 ++--- .../AsyncAnnotationBeanPostProcessor.java | 6 +-- .../validation/DataBinderTests.java | 6 +-- .../beanvalidation/MethodValidationTests.java | 3 +- .../springframework/core/ResolvableType.java | 6 +-- .../htmlunit/MockMvcWebConnectionTests.java | 19 +++---- .../htmlunit/MockWebResponseBuilderTests.java | 12 ++--- .../MockMvcHtmlUnitDriverBuilderTests.java | 1 + .../WebConnectionHtmlUnitDriverTests.java | 1 + ...actFallbackTransactionAttributeSource.java | 30 +++-------- .../TransactionAttributeSource.java | 4 +- .../TransactionAttributeSourcePointcut.java | 10 ++-- 19 files changed, 94 insertions(+), 118 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java index 8b20e4b14822..d59bd01a49a4 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,12 +29,10 @@ import org.springframework.lang.Nullable; /** - * Abstract implementation of {@link JCacheOperationSource} that caches attributes + * Abstract implementation of {@link JCacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. declaring method. * - *

    This implementation caches attributes by method after they are first used. - * * @author Stephane Nicoll * @author Juergen Hoeller * @since 4.1 @@ -43,24 +41,25 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Object NULL_CACHING_ATTRIBUTE = new Object(); + private static final Object NULL_CACHING_MARKER = new Object(); protected final Log logger = LogFactory.getLog(getClass()); - private final Map cache = new ConcurrentHashMap<>(1024); + private final Map operationCache = new ConcurrentHashMap<>(1024); @Override + @Nullable public JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { MethodClassKey cacheKey = new MethodClassKey(method, targetClass); - Object cached = this.cache.get(cacheKey); + Object cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? (JCacheOperation) cached : null); + return (cached != NULL_CACHING_MARKER ? (JCacheOperation) cached : null); } else { JCacheOperation operation = computeCacheOperation(method, targetClass); @@ -68,10 +67,10 @@ public JCacheOperation getCacheOperation(Method method, @Nullable Class ta if (logger.isDebugEnabled()) { logger.debug("Adding cacheable method '" + method.getName() + "' with operation: " + operation); } - this.cache.put(cacheKey, operation); + this.operationCache.put(cacheKey, operation); } else { - this.cache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return operation; } @@ -84,7 +83,7 @@ private JCacheOperation computeCacheOperation(Method method, @Nullable Class< return null; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java index b289b14715f4..ba7b5d8e9c19 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -212,10 +212,8 @@ protected String generateDefaultCacheName(Method method) { for (Class parameterType : parameterTypes) { parameters.add(parameterType.getName()); } - - return method.getDeclaringClass().getName() - + '.' + method.getName() - + '(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')'; + return method.getDeclaringClass().getName() + '.' + method.getName() + + '(' + StringUtils.collectionToCommaDelimitedString(parameters) + ')'; } private int countNonNull(Object... instances) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java index 51fda366b04b..54e4dcaefb6b 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -46,6 +46,7 @@ public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactory * Set the cache operation attribute source which is used to find cache * attributes. This should usually be identical to the source reference * set on the cache interceptor itself. + * @see JCacheInterceptor#setCacheOperationSource */ public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { this.pointcut.setCacheOperationSource(cacheOperationSource); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java index 445a7ef82824..2aa8c2ddb9f0 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -34,7 +34,7 @@ public interface JCacheOperationSource { * Return the cache operations for this method, or {@code null} * if the method contains no JSR-107 related metadata. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * the declaring class of the method must be used) * @return the cache operation for this method, or {@code null} if none found */ diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java index 34693866eea2..a61779b05170 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -24,7 +24,7 @@ import org.springframework.util.ObjectUtils; /** - * A Pointcut that matches if the underlying {@link JCacheOperationSource} + * A {@code Pointcut} that matches if the underlying {@link JCacheOperationSource} * has an operation for a given method. * * @author Stephane Nicoll diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index d20993ae27a7..2883826495f8 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,20 +32,16 @@ import org.springframework.util.ClassUtils; /** - * Abstract implementation of {@link CacheOperation} that caches attributes + * Abstract implementation of {@link CacheOperationSource} that caches operations * for methods and implements a fallback policy: 1. specific target method; * 2. target class; 3. declaring method; 4. declaring class/interface. * - *

    Defaults to using the target class's caching attribute if none is - * associated with the target method. Any caching attribute associated with - * the target method completely overrides a class caching attribute. + *

    Defaults to using the target class's declared cache operations if none are + * associated with the target method. Any cache operations associated with + * the target method completely override any class-level declarations. * If none found on the target class, the interface that the invoked method * has been called through (in case of a JDK proxy) will be checked. * - *

    This implementation caches attributes by method after they are first - * used. If it is ever desirable to allow dynamic changing of cacheable - * attributes (which is very unlikely), caching could be made configurable. - * * @author Costin Leau * @author Juergen Hoeller * @since 3.1 @@ -53,10 +49,10 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource { /** - * Canonical value held in cache to indicate no caching attribute was - * found for this method and we don't need to look again. + * Canonical value held in cache to indicate no cache operation was + * found for this method, and we don't need to look again. */ - private static final Collection NULL_CACHING_ATTRIBUTE = Collections.emptyList(); + private static final Collection NULL_CACHING_MARKER = Collections.emptyList(); /** @@ -71,14 +67,14 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera *

    As this base class is not marked Serializable, the cache will be recreated * after serialization - provided that the concrete subclass is Serializable. */ - private final Map> attributeCache = new ConcurrentHashMap<>(1024); + private final Map> operationCache = new ConcurrentHashMap<>(1024); /** - * Determine the caching attribute for this method invocation. - *

    Defaults to the class's caching attribute if no method attribute is found. + * Determine the cache operations for this method invocation. + *

    Defaults to class-declared metadata if no method-level metadata is found. * @param method the method for the current invocation (never {@code null}) - * @param targetClass the target class for this invocation (may be {@code null}) + * @param targetClass the target class for this invocation (can be {@code null}) * @return {@link CacheOperation} for this method, or {@code null} if the method * is not cacheable */ @@ -90,21 +86,21 @@ public Collection getCacheOperations(Method method, @Nullable Cl } Object cacheKey = getCacheKey(method, targetClass); - Collection cached = this.attributeCache.get(cacheKey); + Collection cached = this.operationCache.get(cacheKey); if (cached != null) { - return (cached != NULL_CACHING_ATTRIBUTE ? cached : null); + return (cached != NULL_CACHING_MARKER ? cached : null); } else { Collection cacheOps = computeCacheOperations(method, targetClass); if (cacheOps != null) { if (logger.isTraceEnabled()) { - logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps); + logger.trace("Adding cacheable method '" + method.getName() + "' with operations: " + cacheOps); } - this.attributeCache.put(cacheKey, cacheOps); + this.operationCache.put(cacheKey, cacheOps); } else { - this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); + this.operationCache.put(cacheKey, NULL_CACHING_MARKER); } return cacheOps; } @@ -129,7 +125,7 @@ private Collection computeCacheOperations(Method method, @Nullab return null; } - // The method may be on an interface, but we need attributes from the target class. + // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); @@ -163,19 +159,19 @@ private Collection computeCacheOperations(Method method, @Nullab /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given class, if any. - * @param clazz the class to retrieve the attribute for - * @return all caching attribute associated with this class, or {@code null} if none + * @param clazz the class to retrieve the cache operations for + * @return all cache operations associated with this class, or {@code null} if none */ @Nullable protected abstract Collection findCacheOperations(Class clazz); /** - * Subclasses need to implement this to return the caching attribute for the + * Subclasses need to implement this to return the cache operations for the * given method, if any. - * @param method the method to retrieve the attribute for - * @return all caching attribute associated with this method, or {@code null} if none + * @param method the method to retrieve the cache operations for + * @return all cache operations associated with this method, or {@code null} if none */ @Nullable protected abstract Collection findCacheOperations(Method method); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java index 02a9b4f41646..7316831e2497 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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,7 +54,7 @@ default boolean isCandidateClass(Class targetClass) { * Return the collection of cache operations for this method, * or {@code null} if the method contains no cacheable annotations. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, in which case + * @param targetClass the target class (can be {@code null}, in which case * the declaring class of the method must be used) * @return all cache operations for this method, or {@code null} if none found */ diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java index e70275aeaed7..6fe6d7c46cfb 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -28,7 +28,7 @@ /** * A {@code Pointcut} that matches if the underlying {@link CacheOperationSource} - * has an attribute for a given method. + * has an operation for a given method. * * @author Costin Leau * @author Juergen Hoeller @@ -36,7 +36,7 @@ * @since 3.1 */ @SuppressWarnings("serial") -class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { +final class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { @Nullable private CacheOperationSource cacheOperationSource; @@ -78,7 +78,7 @@ public String toString() { * {@link ClassFilter} that delegates to {@link CacheOperationSource#isCandidateClass} * for filtering classes whose methods are not worth searching to begin with. */ - private class CacheOperationSourceClassFilter implements ClassFilter { + private final class CacheOperationSourceClassFilter implements ClassFilter { @Override public boolean matches(Class clazz) { @@ -88,6 +88,7 @@ public boolean matches(Class clazz) { return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); } + @Nullable private CacheOperationSource getCacheOperationSource() { return cacheOperationSource; } @@ -95,7 +96,7 @@ private CacheOperationSource getCacheOperationSource() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof CacheOperationSourceClassFilter that && - ObjectUtils.nullSafeEquals(cacheOperationSource, that.getCacheOperationSource()))); + ObjectUtils.nullSafeEquals(getCacheOperationSource(), that.getCacheOperationSource()))); } @Override @@ -105,9 +106,8 @@ public int hashCode() { @Override public String toString() { - return CacheOperationSourceClassFilter.class.getName() + ": " + cacheOperationSource; + return CacheOperationSourceClassFilter.class.getName() + ": " + getCacheOperationSource(); } - } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java index f20aba5585d4..9d8f801590eb 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -98,8 +98,8 @@ public AsyncAnnotationBeanPostProcessor() { * applying the corresponding default if a supplier is not resolvable. * @since 5.1 */ - public void configure( - @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { + public void configure(@Nullable Supplier executor, + @Nullable Supplier exceptionHandler) { this.executor = executor; this.exceptionHandler = exceptionHandler; diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 037dc8d214a3..e21ba65719ce 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -1984,7 +1984,7 @@ void setAutoGrowCollectionLimitAfterInitialization() { .withMessageContaining("DataBinder is already initialized - call setAutoGrowCollectionLimit before other configuration methods"); } - @Test // SPR-15009 + @Test // SPR-15009 void setCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -2001,7 +2001,7 @@ void setCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAc assertThat(((BeanWrapper) binder.getInternalBindingResult().getPropertyAccessor()).getAutoGrowCollectionLimit()).isEqualTo(512); } - @Test // SPR-15009 + @Test // SPR-15009 void setCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -2055,7 +2055,7 @@ void callSetMessageCodesResolverTwice() { .withMessageContaining("DataBinder is already initialized with MessageCodesResolver"); } - @Test // gh-24347 + @Test // gh-24347 void overrideBindingResultType() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java index db7a9b0d05cc..82b6be88ca19 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java @@ -78,8 +78,7 @@ public void testMethodValidationPostProcessor() { ac.close(); } - @Test // gh-29782 - @SuppressWarnings("unchecked") + @Test // gh-29782 public void testMethodValidationPostProcessorForInterfaceOnlyProxy() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MethodValidationPostProcessor.class); diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index c7403097d30b..7fc4b54bc441 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -647,7 +647,7 @@ public ResolvableType getNested(int nestingLevel) { * @param nestingLevel the required nesting level, indexed from 1 for the * current type, 2 for the first nested generic, 3 for the second and so on * @param typeIndexesPerLevel a map containing the generic index for a given - * nesting level (may be {@code null}) + * nesting level (can be {@code null}) * @return a {@code ResolvableType} for the nested level, or {@link #NONE} */ public ResolvableType getNested(int nestingLevel, @Nullable Map typeIndexesPerLevel) { @@ -679,7 +679,7 @@ public ResolvableType getNested(int nestingLevel, @Nullable MapIf no generic is available at the specified indexes {@link #NONE} is returned. * @param indexes the indexes that refer to the generic parameter - * (may be omitted to return the first generic) + * (can be omitted to return the first generic) * @return a {@code ResolvableType} for the specified generic, or {@link #NONE} * @see #hasGenerics() * @see #getGenerics() @@ -782,7 +782,7 @@ public Class[] resolveGenerics(Class fallback) { * Convenience method that will {@link #getGeneric(int...) get} and * {@link #resolve() resolve} a specific generic parameter. * @param indexes the indexes that refer to the generic parameter - * (may be omitted to return the first generic) + * (can be omitted to return the first generic) * @return a resolved {@link Class} or {@code null} * @see #getGeneric(int...) * @see #resolve() diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java index d370005b8cca..7599b7e7df1f 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionTests.java @@ -31,7 +31,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - /** * Integration tests for {@link MockMvcWebConnection}. * @@ -64,14 +63,15 @@ public void contextPathExplicit() throws IOException { public void contextPathEmpty() throws IOException { this.webClient.setWebConnection(new MockMvcWebConnection(this.mockMvc, this.webClient, "")); // Empty context path (root context) should not match to a URL with a context path - assertThatExceptionOfType(FailingHttpStatusCodeException.class).isThrownBy(() -> - this.webClient.getPage("http://localhost/context/a")) - .satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(404)); + assertThatExceptionOfType(FailingHttpStatusCodeException.class) + .isThrownBy(() -> this.webClient.getPage("http://localhost/context/a")) + .satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(404)); + this.webClient.setWebConnection(new MockMvcWebConnection(this.mockMvc, this.webClient)); // No context is the same providing an empty context path - assertThatExceptionOfType(FailingHttpStatusCodeException.class).isThrownBy(() -> - this.webClient.getPage("http://localhost/context/a")) - .satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(404)); + assertThatExceptionOfType(FailingHttpStatusCodeException.class) + .isThrownBy(() -> this.webClient.getPage("http://localhost/context/a")) + .satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(404)); } @Test @@ -84,8 +84,9 @@ public void forward() throws IOException { @Test public void infiniteForward() { this.webClient.setWebConnection(new MockMvcWebConnection(this.mockMvc, this.webClient, "")); - assertThatIllegalStateException().isThrownBy(() -> this.webClient.getPage("http://localhost/infiniteForward")) - .withMessage("Forwarded 100 times in a row, potential infinite forward loop"); + assertThatIllegalStateException() + .isThrownBy(() -> this.webClient.getPage("http://localhost/infiniteForward")) + .withMessage("Forwarded 100 times in a row, potential infinite forward loop"); } @Test diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java index dd72b0d7b232..46f465b8fba1 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java @@ -32,7 +32,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - /** * Tests for {@link MockWebResponseBuilder}. * @@ -55,8 +54,6 @@ public void setup() throws Exception { } - // --- constructor - @Test public void constructorWithNullWebRequest() { assertThatIllegalArgumentException().isThrownBy(() -> @@ -66,12 +63,10 @@ public void constructorWithNullWebRequest() { @Test public void constructorWithNullResponse() throws Exception { assertThatIllegalArgumentException().isThrownBy(() -> - new MockWebResponseBuilder(0L, new WebRequest(new URL("http://company.example:80/test/this/here")), null)); + new MockWebResponseBuilder(0L, + new WebRequest(new URL("http://company.example:80/test/this/here")), null)); } - - // --- build - @Test public void buildContent() throws Exception { this.response.getWriter().write("expected content"); @@ -124,8 +119,7 @@ public void buildResponseHeaders() throws Exception { .endsWith("; Secure; HttpOnly"); } - // SPR-14169 - @Test + @Test // SPR-14169 public void buildResponseHeadersNullDomainDefaulted() throws Exception { Cookie cookie = new Cookie("cookieA", "valueA"); this.response.addCookie(cookie); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java index 9edb7bd6ee12..fc13936fba4a 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilderTests.java @@ -52,6 +52,7 @@ class MockMvcHtmlUnitDriverBuilderTests { private HtmlUnitDriver driver; + MockMvcHtmlUnitDriverBuilderTests(WebApplicationContext wac) { this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java index fbe473762483..4c125855820c 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriverTests.java @@ -48,6 +48,7 @@ class WebConnectionHtmlUnitDriverTests { @Mock private WebConnection connection; + @BeforeEach void setup() throws Exception { given(this.connection.getResponse(any(WebRequest.class))).willThrow(new IOException("")); diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java index e669818c3624..6c9a243d4d7e 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -42,11 +42,6 @@ * If none found on the target class, the interface that the invoked method * has been called through (in case of a JDK proxy) will be checked. * - *

    This implementation caches attributes by method after they are first used. - * If it is ever desirable to allow dynamic changing of transaction attributes - * (which is very unlikely), caching could be made configurable. Caching is - * desirable because of the cost of evaluating rollback rules. - * * @author Rod Johnson * @author Juergen Hoeller * @since 1.1 @@ -95,7 +90,7 @@ public void setEmbeddedValueResolver(StringValueResolver resolver) { * Determine the transaction attribute for this method invocation. *

    Defaults to the class's transaction attribute if no method attribute is found. * @param method the method for the current invocation (never {@code null}) - * @param targetClass the target class for this invocation (may be {@code null}) + * @param targetClass the target class for this invocation (can be {@code null}) * @return a TransactionAttribute for this method, or {@code null} if the method * is not transactional */ @@ -106,27 +101,15 @@ public TransactionAttribute getTransactionAttribute(Method method, @Nullable Cla return null; } - // First, see if we have a cached value. Object cacheKey = getCacheKey(method, targetClass); TransactionAttribute cached = this.attributeCache.get(cacheKey); + if (cached != null) { - // Value will either be canonical value indicating there is no transaction attribute, - // or an actual transaction attribute. - if (cached == NULL_TRANSACTION_ATTRIBUTE) { - return null; - } - else { - return cached; - } + return (cached != NULL_TRANSACTION_ATTRIBUTE ? cached : null); } else { - // We need to work it out. TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); - // Put it in the cache. - if (txAttr == null) { - this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); - } - else { + if (txAttr != null) { String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); if (txAttr instanceof DefaultTransactionAttribute dta) { dta.setDescriptor(methodIdentification); @@ -137,6 +120,9 @@ public TransactionAttribute getTransactionAttribute(Method method, @Nullable Cla } this.attributeCache.put(cacheKey, txAttr); } + else { + this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); + } return txAttr; } } diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java index 329f53420527..74eb470229f9 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -57,7 +57,7 @@ default boolean isCandidateClass(Class targetClass) { * Return the transaction attribute for the given method, * or {@code null} if the method is non-transactional. * @param method the method to introspect - * @param targetClass the target class (may be {@code null}, + * @param targetClass the target class (can be {@code null}, * in which case the declaring class of the method must be used) * @return the matching transaction attribute, or {@code null} if none found */ diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java index 10ac08147ae3..d388850da43b 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAttributeSourcePointcut.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -77,7 +77,7 @@ public String toString() { * {@link ClassFilter} that delegates to {@link TransactionAttributeSource#isCandidateClass} * for filtering classes whose methods are not worth searching to begin with. */ - private class TransactionAttributeSourceClassFilter implements ClassFilter { + private final class TransactionAttributeSourceClassFilter implements ClassFilter { @Override public boolean matches(Class clazz) { @@ -89,6 +89,7 @@ public boolean matches(Class clazz) { return (transactionAttributeSource == null || transactionAttributeSource.isCandidateClass(clazz)); } + @Nullable private TransactionAttributeSource getTransactionAttributeSource() { return transactionAttributeSource; } @@ -96,7 +97,7 @@ private TransactionAttributeSource getTransactionAttributeSource() { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof TransactionAttributeSourceClassFilter that && - ObjectUtils.nullSafeEquals(transactionAttributeSource, that.getTransactionAttributeSource()))); + ObjectUtils.nullSafeEquals(getTransactionAttributeSource(), that.getTransactionAttributeSource()))); } @Override @@ -106,9 +107,8 @@ public int hashCode() { @Override public String toString() { - return TransactionAttributeSourceClassFilter.class.getName() + ": " + transactionAttributeSource; + return TransactionAttributeSourceClassFilter.class.getName() + ": " + getTransactionAttributeSource(); } - } } From 9ac1feceb559280dd8bf9a1e633a489d53fe6ba0 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 4 Mar 2024 23:32:27 +0100 Subject: [PATCH 120/261] Restore ability to return original method for proxy-derived method Closes gh-32365 --- .../springframework/aop/support/AopUtils.java | 8 +-- .../aop/support/AopUtilsTests.java | 59 +++++++++++++++++-- .../org/springframework/util/ClassUtils.java | 29 ++++----- 3 files changed, 72 insertions(+), 24 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java index dcc8670f8706..6cdaa7d2e6b7 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -186,10 +186,10 @@ public static boolean isFinalizeMethod(@Nullable Method method) { * this method resolves bridge methods in order to retrieve attributes from * the original method definition. * @param method the method to be invoked, which may come from an interface - * @param targetClass the target class for the current invocation. - * May be {@code null} or may not even implement the method. + * @param targetClass the target class for the current invocation + * (can be {@code null} or may not even implement the method) * @return the specific target method, or the original method if the - * {@code targetClass} doesn't implement it or is {@code null} + * {@code targetClass} does not implement it * @see org.springframework.util.ClassUtils#getMostSpecificMethod */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { diff --git a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java index dc5437e6cd67..fb4d1ed6f717 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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,29 +17,35 @@ package org.springframework.aop.support; import java.lang.reflect.Method; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.aop.target.EmptyTargetSource; import org.springframework.aop.testfixture.interceptor.NopInterceptor; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.core.ResolvableType; import org.springframework.core.testfixture.io.SerializationTestUtils; import org.springframework.lang.Nullable; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; /** * @author Rod Johnson * @author Chris Beams + * @author Sebastien Deleuze + * @author Juergen Hoeller */ -public class AopUtilsTests { +class AopUtilsTests { @Test - public void testPointcutCanNeverApply() { + void testPointcutCanNeverApply() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazzy) { @@ -52,13 +58,13 @@ public boolean matches(Method method, @Nullable Class clazzy) { } @Test - public void testPointcutAlwaysApplies() { + void testPointcutAlwaysApplies() { assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue(); assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue(); } @Test - public void testPointcutAppliesToOneMethodOnObject() { + void testPointcutAppliesToOneMethodOnObject() { class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, @Nullable Class clazz) { @@ -78,7 +84,7 @@ public boolean matches(Method method, @Nullable Class clazz) { * that's subverted the singleton construction limitation. */ @Test - public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { + void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception { assertThat(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)).isSameAs(MethodMatcher.TRUE); assertThat(SerializationTestUtils.serializeAndDeserialize(ClassFilter.TRUE)).isSameAs(ClassFilter.TRUE); assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcut.TRUE)).isSameAs(Pointcut.TRUE); @@ -88,4 +94,45 @@ public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throw assertThat(SerializationTestUtils.serializeAndDeserialize(ExposeInvocationInterceptor.INSTANCE)).isSameAs(ExposeInvocationInterceptor.INSTANCE); } + @Test + void testInvokeJoinpointUsingReflection() throws Throwable { + String name = "foo"; + TestBean testBean = new TestBean(name); + Method method = ReflectionUtils.findMethod(TestBean.class, "getName"); + Object result = AopUtils.invokeJoinpointUsingReflection(testBean, method, new Object[0]); + assertThat(result).isEqualTo(name); + } + + @Test // gh-32365 + void mostSpecificMethodBetweenJdkProxyAndTarget() throws Exception { + Class proxyClass = new ProxyFactory(new WithInterface()).getProxyClass(getClass().getClassLoader()); + Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithInterface.class); + assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class); + } + + @Test // gh-32365 + void mostSpecificMethodBetweenCglibProxyAndTarget() throws Exception { + Class proxyClass = new ProxyFactory(new WithoutInterface()).getProxyClass(getClass().getClassLoader()); + Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithoutInterface.class); + assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class); + } + + + interface ProxyInterface { + + void handle(List list); + } + + static class WithInterface implements ProxyInterface { + + public void handle(List list) { + } + } + + static class WithoutInterface { + + public void handle(List list) { + } + } + } diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index e4d9a4ca0b68..e35fe0642a46 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -243,7 +243,7 @@ public static ClassLoader overrideThreadContextClassLoader(@Nullable ClassLoader * style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State"). * @param name the name of the Class * @param classLoader the class loader to use - * (may be {@code null}, which indicates the default class loader) + * (can be {@code null}, which indicates the default class loader) * @return a class instance for the supplied name * @throws ClassNotFoundException if the class was not found * @throws LinkageError if the class file could not be loaded @@ -314,7 +314,7 @@ public static Class forName(String name, @Nullable ClassLoader classLoader) * the exceptions thrown in case of class loading failure. * @param className the name of the Class * @param classLoader the class loader to use - * (may be {@code null}, which indicates the default class loader) + * (can be {@code null}, which indicates the default class loader) * @return a class instance for the supplied name * @throws IllegalArgumentException if the class name was not resolvable * (that is, the class could not be found or the class file could not be loaded) @@ -348,7 +348,7 @@ public static Class resolveClassName(String className, @Nullable ClassLoader * one of its dependencies is not present or cannot be loaded. * @param className the name of the class to check * @param classLoader the class loader to use - * (may be {@code null} which indicates the default class loader) + * (can be {@code null} which indicates the default class loader) * @return whether the specified class is present (including all of its * superclasses and interfaces) * @throws IllegalStateException if the corresponding class is resolvable but @@ -375,7 +375,7 @@ public static boolean isPresent(String className, @Nullable ClassLoader classLoa * Check whether the given class is visible in the given ClassLoader. * @param clazz the class to check (typically an interface) * @param classLoader the ClassLoader to check against - * (may be {@code null} in which case this method will always return {@code true}) + * (can be {@code null} in which case this method will always return {@code true}) */ public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoader) { if (classLoader == null) { @@ -399,7 +399,7 @@ public static boolean isVisible(Class clazz, @Nullable ClassLoader classLoade * i.e. whether it is loaded by the given ClassLoader or a parent of it. * @param clazz the class to analyze * @param classLoader the ClassLoader to potentially cache metadata in - * (may be {@code null} which indicates the system class loader) + * (can be {@code null} which indicates the system class loader) */ public static boolean isCacheSafe(Class clazz, @Nullable ClassLoader classLoader) { Assert.notNull(clazz, "Class must not be null"); @@ -663,7 +663,7 @@ public static String classNamesToString(Class... classes) { * in the given collection. *

    Basically like {@code AbstractCollection.toString()}, but stripping * the "class "/"interface " prefix before every class name. - * @param classes a Collection of Class objects (may be {@code null}) + * @param classes a Collection of Class objects (can be {@code null}) * @return a String of form "[com.foo.Bar, com.foo.Baz]" * @see java.util.AbstractCollection#toString() */ @@ -718,7 +718,7 @@ public static Class[] getAllInterfacesForClass(Class clazz) { *

    If the class itself is an interface, it gets returned as sole interface. * @param clazz the class to analyze for interfaces * @param classLoader the ClassLoader that the interfaces need to be visible in - * (may be {@code null} when accepting all declared interfaces) + * (can be {@code null} when accepting all declared interfaces) * @return all interfaces that the given object implements as an array */ public static Class[] getAllInterfacesForClass(Class clazz, @Nullable ClassLoader classLoader) { @@ -753,7 +753,7 @@ public static Set> getAllInterfacesForClassAsSet(Class clazz) { *

    If the class itself is an interface, it gets returned as sole interface. * @param clazz the class to analyze for interfaces * @param classLoader the ClassLoader that the interfaces need to be visible in - * (may be {@code null} when accepting all declared interfaces) + * (can be {@code null} when accepting all declared interfaces) * @return all interfaces that the given object implements as a Set */ public static Set> getAllInterfacesForClassAsSet(Class clazz, @Nullable ClassLoader classLoader) { @@ -1082,7 +1082,7 @@ public static String getQualifiedMethodName(Method method) { * fully qualified interface/class name + "." + method name. * @param method the method * @param clazz the clazz that the method is being invoked on - * (may be {@code null} to indicate the method's declaring class) + * (can be {@code null} to indicate the method's declaring class) * @return the qualified name of the method * @since 4.3.4 */ @@ -1163,7 +1163,7 @@ public static boolean hasMethod(Class clazz, String methodName, Class... p * @param clazz the clazz to analyze * @param methodName the name of the method * @param paramTypes the parameter types of the method - * (may be {@code null} to indicate any signature) + * (can be {@code null} to indicate any signature) * @return the method (never {@code null}) * @throws IllegalStateException if the method has not been found * @see Class#getMethod @@ -1202,7 +1202,7 @@ else if (candidates.isEmpty()) { * @param clazz the clazz to analyze * @param methodName the name of the method * @param paramTypes the parameter types of the method - * (may be {@code null} to indicate any signature) + * (can be {@code null} to indicate any signature) * @return the method, or {@code null} if not found * @see Class#getMethod */ @@ -1291,13 +1291,14 @@ public static boolean hasAtLeastOneMethodWithName(Class clazz, String methodN * implementation will fall back to returning the originally provided method. * @param method the method to be invoked, which may come from an interface * @param targetClass the target class for the current invocation - * (may be {@code null} or may not even implement the method) + * (can be {@code null} or may not even implement the method) * @return the specific target method, or the original method if the * {@code targetClass} does not implement it * @see #getInterfaceMethodIfPossible(Method, Class) */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { - if (targetClass != null && targetClass != method.getDeclaringClass() && isOverridable(method, targetClass)) { + if (targetClass != null && targetClass != method.getDeclaringClass() && + (isOverridable(method, targetClass) || !method.getDeclaringClass().isAssignableFrom(targetClass))) { try { if (Modifier.isPublic(method.getModifiers())) { try { From 1a7a6f421fee4b2c53fc86d39b748c363ba80e0c Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 5 Mar 2024 12:03:48 +0000 Subject: [PATCH 121/261] Backport tests for wrapping of response for async requests This is a backport of commits 4b96cd and ef0717. Closes gh-32341 --- .../async/StandardServletAsyncWebRequest.java | 263 +++++++------ .../request/async/WebAsyncManager.java | 5 +- .../async/AsyncRequestNotUsableTests.java | 357 ++++++++++++++++++ .../RequestMappingHandlerAdapterTests.java | 44 ++- 4 files changed, 545 insertions(+), 124 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestNotUsableTests.java diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java index aa48427b2ad6..67e137a55d5e 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/StandardServletAsyncWebRequest.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -189,7 +188,7 @@ public void onTimeout(AsyncEvent event) throws IOException { public void onError(AsyncEvent event) throws IOException { this.stateLock.lock(); try { - transitionToErrorState(); + this.state = State.ERROR; Throwable ex = event.getThrowable(); this.exceptionHandlers.forEach(consumer -> consumer.accept(ex)); } @@ -198,12 +197,6 @@ public void onError(AsyncEvent event) throws IOException { } } - private void transitionToErrorState() { - if (!isAsyncComplete()) { - this.state = State.ERROR; - } - } - @Override public void onComplete(AsyncEvent event) throws IOException { this.stateLock.lock(); @@ -218,8 +211,17 @@ public void onComplete(AsyncEvent event) throws IOException { } + /** + * Package private access for testing only. + */ + ReentrantLock stateLock() { + return this.stateLock; + } + + /** * Response wrapper to wrap the output stream with {@link LifecycleServletOutputStream}. + * @since 5.3.33 */ private static final class LifecycleHttpServletResponse extends HttpServletResponseWrapper { @@ -241,145 +243,180 @@ public void setAsyncWebRequest(StandardServletAsyncWebRequest asyncWebRequest) { } @Override - public ServletOutputStream getOutputStream() { - if (this.outputStream == null) { - Assert.notNull(this.asyncWebRequest, "Not initialized"); - this.outputStream = new LifecycleServletOutputStream( - (HttpServletResponse) getResponse(), this.asyncWebRequest); + public ServletOutputStream getOutputStream() throws IOException { + int level = obtainLockAndCheckState(); + try { + if (this.outputStream == null) { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + ServletOutputStream delegate = getResponse().getOutputStream(); + this.outputStream = new LifecycleServletOutputStream(delegate, this); + } + } + catch (IOException ex) { + handleIOException(ex, "Failed to get ServletResponseOutput"); + } + finally { + releaseLock(level); } return this.outputStream; } @Override public PrintWriter getWriter() throws IOException { - if (this.writer == null) { - Assert.notNull(this.asyncWebRequest, "Not initialized"); - this.writer = new LifecyclePrintWriter(getResponse().getWriter(), this.asyncWebRequest); + int level = obtainLockAndCheckState(); + try { + if (this.writer == null) { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + this.writer = new LifecyclePrintWriter(getResponse().getWriter(), this.asyncWebRequest); + } + } + catch (IOException ex) { + handleIOException(ex, "Failed to get PrintWriter"); + } + finally { + releaseLock(level); } return this.writer; } + + @Override + public void flushBuffer() throws IOException { + int level = obtainLockAndCheckState(); + try { + getResponse().flushBuffer(); + } + catch (IOException ex) { + handleIOException(ex, "ServletResponse failed to flushBuffer"); + } + finally { + releaseLock(level); + } + } + + /** + * Return 0 if checks passed and lock is not needed, 1 if checks passed + * and lock is held, or raise AsyncRequestNotUsableException. + */ + private int obtainLockAndCheckState() throws AsyncRequestNotUsableException { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + if (this.asyncWebRequest.state == State.NEW) { + return 0; + } + + this.asyncWebRequest.stateLock.lock(); + if (this.asyncWebRequest.state == State.ASYNC) { + return 1; + } + + this.asyncWebRequest.stateLock.unlock(); + throw new AsyncRequestNotUsableException("Response not usable after " + + (this.asyncWebRequest.state == State.COMPLETED ? + "async request completion" : "response errors") + "."); + } + + void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + this.asyncWebRequest.state = State.ERROR; + throw new AsyncRequestNotUsableException(msg + ": " + ex.getMessage(), ex); + } + + void releaseLock(int level) { + Assert.notNull(this.asyncWebRequest, "Not initialized"); + if (level > 0) { + this.asyncWebRequest.stateLock.unlock(); + } + } } /** * Wraps a ServletOutputStream to prevent use after Servlet container onError * notifications, and after async request completion. + * @since 5.3.33 */ private static final class LifecycleServletOutputStream extends ServletOutputStream { - private final HttpServletResponse delegate; - - private final StandardServletAsyncWebRequest asyncWebRequest; + private final ServletOutputStream delegate; - private LifecycleServletOutputStream( - HttpServletResponse delegate, StandardServletAsyncWebRequest asyncWebRequest) { + private final LifecycleHttpServletResponse response; + private LifecycleServletOutputStream(ServletOutputStream delegate, LifecycleHttpServletResponse response) { this.delegate = delegate; - this.asyncWebRequest = asyncWebRequest; + this.response = response; } @Override public boolean isReady() { - return false; + return this.delegate.isReady(); } @Override public void setWriteListener(WriteListener writeListener) { - throw new UnsupportedOperationException(); + this.delegate.setWriteListener(writeListener); } @Override public void write(int b) throws IOException { - obtainLockAndCheckState(); + int level = this.response.obtainLockAndCheckState(); try { - this.delegate.getOutputStream().write(b); + this.delegate.write(b); } catch (IOException ex) { - handleIOException(ex, "ServletOutputStream failed to write"); + this.response.handleIOException(ex, "ServletOutputStream failed to write"); } finally { - releaseLock(); + this.response.releaseLock(level); } } public void write(byte[] buf, int offset, int len) throws IOException { - obtainLockAndCheckState(); + int level = this.response.obtainLockAndCheckState(); try { - this.delegate.getOutputStream().write(buf, offset, len); + this.delegate.write(buf, offset, len); } catch (IOException ex) { - handleIOException(ex, "ServletOutputStream failed to write"); + this.response.handleIOException(ex, "ServletOutputStream failed to write"); } finally { - releaseLock(); + this.response.releaseLock(level); } } @Override public void flush() throws IOException { - obtainLockAndCheckState(); + int level = this.response.obtainLockAndCheckState(); try { - this.delegate.getOutputStream().flush(); + this.delegate.flush(); } catch (IOException ex) { - handleIOException(ex, "ServletOutputStream failed to flush"); + this.response.handleIOException(ex, "ServletOutputStream failed to flush"); } finally { - releaseLock(); + this.response.releaseLock(level); } } @Override public void close() throws IOException { - obtainLockAndCheckState(); + int level = this.response.obtainLockAndCheckState(); try { - this.delegate.getOutputStream().close(); + this.delegate.close(); } catch (IOException ex) { - handleIOException(ex, "ServletOutputStream failed to close"); + this.response.handleIOException(ex, "ServletOutputStream failed to close"); } finally { - releaseLock(); + this.response.releaseLock(level); } } - private void obtainLockAndCheckState() throws AsyncRequestNotUsableException { - if (state() != State.NEW) { - stateLock().lock(); - if (state() != State.ASYNC) { - stateLock().unlock(); - throw new AsyncRequestNotUsableException("Response not usable after " + - (state() == State.COMPLETED ? - "async request completion" : "onError notification") + "."); - } - } - } - - private void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException { - this.asyncWebRequest.transitionToErrorState(); - throw new AsyncRequestNotUsableException(msg, ex); - } - - private void releaseLock() { - if (state() != State.NEW) { - stateLock().unlock(); - } - } - - private State state() { - return this.asyncWebRequest.state; - } - - private Lock stateLock() { - return this.asyncWebRequest.stateLock; - } - } /** * Wraps a PrintWriter to prevent use after Servlet container onError * notifications, and after async request completion. + * @since 5.3.33 */ private static final class LifecyclePrintWriter extends PrintWriter { @@ -395,24 +432,26 @@ private LifecyclePrintWriter(PrintWriter delegate, StandardServletAsyncWebReques @Override public void flush() { - if (tryObtainLockAndCheckState()) { + int level = tryObtainLockAndCheckState(); + if (level > -1) { try { this.delegate.flush(); } finally { - releaseLock(); + releaseLock(level); } } } @Override public void close() { - if (tryObtainLockAndCheckState()) { + int level = tryObtainLockAndCheckState(); + if (level > -1) { try { this.delegate.close(); } finally { - releaseLock(); + releaseLock(level); } } } @@ -424,24 +463,26 @@ public boolean checkError() { @Override public void write(int c) { - if (tryObtainLockAndCheckState()) { + int level = tryObtainLockAndCheckState(); + if (level > -1) { try { this.delegate.write(c); } finally { - releaseLock(); + releaseLock(level); } } } @Override public void write(char[] buf, int off, int len) { - if (tryObtainLockAndCheckState()) { + int level = tryObtainLockAndCheckState(); + if (level > -1) { try { this.delegate.write(buf, off, len); } finally { - releaseLock(); + releaseLock(level); } } } @@ -453,12 +494,13 @@ public void write(char[] buf) { @Override public void write(String s, int off, int len) { - if (tryObtainLockAndCheckState()) { + int level = tryObtainLockAndCheckState(); + if (level > -1) { try { this.delegate.write(s, off, len); } finally { - releaseLock(); + releaseLock(level); } } } @@ -468,33 +510,28 @@ public void write(String s) { this.delegate.write(s); } - private boolean tryObtainLockAndCheckState() { - if (state() == State.NEW) { - return true; + /** + * Return 0 if checks passed and lock is not needed, 1 if checks passed + * and lock is held, and -1 if checks did not pass. + */ + private int tryObtainLockAndCheckState() { + if (this.asyncWebRequest.state == State.NEW) { + return 0; } - if (stateLock().tryLock()) { - if (state() == State.ASYNC) { - return true; - } - stateLock().unlock(); + this.asyncWebRequest.stateLock.lock(); + if (this.asyncWebRequest.state == State.ASYNC) { + return 1; } - return false; + this.asyncWebRequest.stateLock.unlock(); + return -1; } - private void releaseLock() { - if (state() != State.NEW) { - stateLock().unlock(); + private void releaseLock(int level) { + if (level > 0) { + this.asyncWebRequest.stateLock.unlock(); } } - private State state() { - return this.asyncWebRequest.state; - } - - private Lock stateLock() { - return this.asyncWebRequest.stateLock; - } - // Plain delegates @Override @@ -632,28 +669,28 @@ public PrintWriter append(char c) { /** * Represents a state for {@link StandardServletAsyncWebRequest} to be in. *

    -	 *        NEW
    -	 *         |
    -	 *         v
    -	 *       ASYNC----> +
    -	 *         |        |
    -	 *         v        |
    -	 *       ERROR      |
    -	 *         |        |
    -	 *         v        |
    -	 *     COMPLETED <--+
    +	 *    +------ NEW
    +	 *    |        |
    +	 *    |        v
    +	 *    |      ASYNC ----> +
    +	 *    |        |         |
    +	 *    |        v         |
    +	 *    +----> ERROR       |
    +	 *             |         |
    +	 *             v         |
    +	 *         COMPLETED <---+
     	 * 
    * @since 5.3.33 */ private enum State { - /** New request (thas may not do async handling). */ + /** New request (may not start async handling). */ NEW, /** Async handling has started. */ ASYNC, - /** onError notification received, or ServletOutputStream failed. */ + /** ServletOutputStream failed, or onError notification received. */ ERROR, /** onComplete notification received. */ diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java index 5ff3485a670d..56b3d84e5e5e 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/WebAsyncManager.java @@ -514,14 +514,11 @@ private void startAsyncProcessing(Object[] processingContext) { } this.asyncWebRequest.startAsync(); - if (logger.isDebugEnabled()) { - logger.debug("Started async request"); - } } private static String formatUri(AsyncWebRequest asyncWebRequest) { HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class); - return (request != null ? request.getRequestURI() : "servlet container"); + return (request != null ? "\"" + request.getRequestURI() + "\"" : "servlet container"); } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestNotUsableTests.java b/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestNotUsableTests.java new file mode 100644 index 000000000000..0033627dfb9a --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestNotUsableTests.java @@ -0,0 +1,357 @@ +/* + * Copyright 2002-2024 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.context.request.async; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; + +import org.springframework.web.testfixture.servlet.MockAsyncContext; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.doAnswer; +import static org.mockito.BDDMockito.doThrow; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; +import static org.mockito.BDDMockito.verifyNoInteractions; + +/** + * {@link StandardServletAsyncWebRequest} tests related to response wrapping in + * order to enforce thread safety and prevent use after errors. + * + * @author Rossen Stoyanchev + */ +public class AsyncRequestNotUsableTests { + + private final MockHttpServletRequest request = new MockHttpServletRequest(); + + private final HttpServletResponse response = mock(); + + private final ServletOutputStream outputStream = mock(); + + private final PrintWriter writer = mock(); + + private StandardServletAsyncWebRequest asyncRequest; + + + @BeforeEach + void setup() throws IOException { + this.request.setAsyncSupported(true); + given(this.response.getOutputStream()).willReturn(this.outputStream); + given(this.response.getWriter()).willReturn(this.writer); + + this.asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response); + } + + @AfterEach + void tearDown() { + assertThat(this.asyncRequest.stateLock().isLocked()).isFalse(); + } + + + @SuppressWarnings("DataFlowIssue") + private ServletOutputStream getWrappedOutputStream() throws IOException { + return this.asyncRequest.getResponse().getOutputStream(); + } + + @SuppressWarnings("DataFlowIssue") + private PrintWriter getWrappedWriter() throws IOException { + return this.asyncRequest.getResponse().getWriter(); + } + + + @Nested + class ResponseTests { + + @Test + void notUsableAfterError() throws IOException { + asyncRequest.startAsync(); + asyncRequest.onError(new AsyncEvent(new MockAsyncContext(request, response), new Exception())); + + HttpServletResponse wrapped = asyncRequest.getResponse(); + assertThat(wrapped).isNotNull(); + assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after response errors."); + assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after response errors."); + assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after response errors."); + } + + @Test + void notUsableAfterCompletion() throws IOException { + asyncRequest.startAsync(); + asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response))); + + HttpServletResponse wrapped = asyncRequest.getResponse(); + assertThat(wrapped).isNotNull(); + assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after async request completion."); + } + + @Test + void notUsableWhenRecreatedAfterCompletion() throws IOException { + asyncRequest.startAsync(); + asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response))); + + StandardServletAsyncWebRequest newWebRequest = + new StandardServletAsyncWebRequest(request, response, asyncRequest); + + HttpServletResponse wrapped = newWebRequest.getResponse(); + assertThat(wrapped).isNotNull(); + assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after async request completion."); + } + } + + + @Nested + class OutputStreamTests { + + @Test + void use() throws IOException { + testUseOutputStream(); + } + + @Test + void useInAsyncState() throws IOException { + asyncRequest.startAsync(); + testUseOutputStream(); + } + + private void testUseOutputStream() throws IOException { + ServletOutputStream wrapped = getWrappedOutputStream(); + + wrapped.write('a'); + wrapped.write(new byte[0], 1, 2); + wrapped.flush(); + wrapped.close(); + + verify(outputStream).write('a'); + verify(outputStream).write(new byte[0], 1, 2); + verify(outputStream).flush(); + verify(outputStream).close(); + } + + @Test + void notUsableAfterCompletion() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response))); + + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(() -> wrapped.write(new byte[0])).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(() -> wrapped.write(new byte[0], 0, 0)).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::flush).hasMessage("Response not usable after async request completion."); + assertThatThrownBy(wrapped::close).hasMessage("Response not usable after async request completion."); + } + + @Test + void lockingNotUsed() throws IOException { + AtomicInteger count = new AtomicInteger(-1); + doAnswer((Answer) invocation -> { + count.set(asyncRequest.stateLock().getHoldCount()); + return null; + }).when(outputStream).write('a'); + + // Access ServletOutputStream in NEW state (no async handling) without locking + getWrappedOutputStream().write('a'); + + assertThat(count.get()).isEqualTo(0); + } + + @Test + void lockingUsedInAsyncState() throws IOException { + AtomicInteger count = new AtomicInteger(-1); + doAnswer((Answer) invocation -> { + count.set(asyncRequest.stateLock().getHoldCount()); + return null; + }).when(outputStream).write('a'); + + // Access ServletOutputStream in ASYNC state with locking + asyncRequest.startAsync(); + getWrappedOutputStream().write('a'); + + assertThat(count.get()).isEqualTo(1); + } + } + + + @Nested + class OutputStreamErrorTests { + + @Test + void writeInt() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).write('a'); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe"); + } + + @Test + void writeBytesFull() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + byte[] bytes = new byte[0]; + doThrow(new IOException("Broken pipe")).when(outputStream).write(bytes, 0, 0); + assertThatThrownBy(() -> wrapped.write(bytes)).hasMessage("ServletOutputStream failed to write: Broken pipe"); + } + + @Test + void writeBytes() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + byte[] bytes = new byte[0]; + doThrow(new IOException("Broken pipe")).when(outputStream).write(bytes, 0, 0); + assertThatThrownBy(() -> wrapped.write(bytes, 0, 0)).hasMessage("ServletOutputStream failed to write: Broken pipe"); + } + + @Test + void flush() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).flush(); + assertThatThrownBy(wrapped::flush).hasMessage("ServletOutputStream failed to flush: Broken pipe"); + } + + @Test + void close() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).close(); + assertThatThrownBy(wrapped::close).hasMessage("ServletOutputStream failed to close: Broken pipe"); + } + + @Test + void writeErrorPreventsFurtherWriting() throws IOException { + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).write('a'); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe"); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after response errors."); + } + + @Test + void writeErrorInAsyncStatePreventsFurtherWriting() throws IOException { + asyncRequest.startAsync(); + ServletOutputStream wrapped = getWrappedOutputStream(); + + doThrow(new IOException("Broken pipe")).when(outputStream).write('a'); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe"); + assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after response errors."); + } + } + + + @Nested + class WriterTests { + + @Test + void useWriter() throws IOException { + testUseWriter(); + } + + @Test + void useWriterInAsyncState() throws IOException { + asyncRequest.startAsync(); + testUseWriter(); + } + + private void testUseWriter() throws IOException { + PrintWriter wrapped = getWrappedWriter(); + + wrapped.write('a'); + wrapped.write(new char[0], 1, 2); + wrapped.write("abc", 1, 2); + wrapped.flush(); + wrapped.close(); + + verify(writer).write('a'); + verify(writer).write(new char[0], 1, 2); + verify(writer).write("abc", 1, 2); + verify(writer).flush(); + verify(writer).close(); + } + + @Test + void writerNotUsableAfterCompletion() throws IOException { + asyncRequest.startAsync(); + PrintWriter wrapped = getWrappedWriter(); + + asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response))); + + char[] chars = new char[0]; + wrapped.write('a'); + wrapped.write(chars, 1, 2); + wrapped.flush(); + wrapped.close(); + + verifyNoInteractions(writer); + } + + @Test + void lockingNotUsed() throws IOException { + AtomicInteger count = new AtomicInteger(-1); + + doAnswer((Answer) invocation -> { + count.set(asyncRequest.stateLock().getHoldCount()); + return null; + }).when(writer).write('a'); + + // Use Writer in NEW state (no async handling) without locking + PrintWriter wrapped = getWrappedWriter(); + wrapped.write('a'); + + assertThat(count.get()).isEqualTo(0); + } + + @Test + void lockingUsedInAsyncState() throws IOException { + AtomicInteger count = new AtomicInteger(-1); + + doAnswer((Answer) invocation -> { + count.set(asyncRequest.stateLock().getHoldCount()); + return null; + }).when(writer).write('a'); + + // Use Writer in ASYNC state with locking + asyncRequest.startAsync(); + PrintWriter wrapped = getWrappedWriter(); + wrapped.write('a'); + + assertThat(count.get()).isEqualTo(1); + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java index cdf6f16b2839..ea5b5a00ac23 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,8 +16,11 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.io.IOException; +import java.io.OutputStream; import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -25,6 +28,7 @@ import java.util.List; import java.util.Map; +import jakarta.servlet.AsyncEvent; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,6 +50,10 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.SessionAttributes; +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; +import org.springframework.web.context.request.async.StandardServletAsyncWebRequest; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.annotation.ModelMethodProcessor; @@ -55,13 +63,15 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.testfixture.servlet.MockAsyncContext; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** - * Unit tests for {@link RequestMappingHandlerAdapter}. + * Tests for {@link RequestMappingHandlerAdapter}. * * @author Rossen Stoyanchev * @author Sam Brannen @@ -249,9 +259,7 @@ public void modelAttributePackageNameAdvice() throws Exception { assertThat(mav.getModel().get("attr3")).isNull(); } - // SPR-10859 - - @Test + @Test // gh-15486 public void responseBodyAdvice() throws Exception { List> converters = new ArrayList<>(); converters.add(new MappingJackson2HttpMessageConverter()); @@ -271,6 +279,26 @@ public void responseBodyAdvice() throws Exception { assertThat(this.response.getContentAsString()).isEqualTo("{\"status\":400,\"message\":\"body\"}"); } + @Test + void asyncRequestNotUsable() throws Exception { + + // Put AsyncWebRequest in ERROR state + StandardServletAsyncWebRequest asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response); + asyncRequest.onError(new AsyncEvent(new MockAsyncContext(this.request, this.response), new Exception())); + + // Set it as the current AsyncWebRequest, from the initial REQUEST dispatch + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(this.request); + asyncManager.setAsyncWebRequest(asyncRequest); + + // AsyncWebRequest created for current dispatch should inherit state + HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleOutputStream", OutputStream.class); + this.handlerAdapter.afterPropertiesSet(); + + // Use of response should be rejected + assertThatThrownBy(() -> this.handlerAdapter.handle(this.request, this.response, handlerMethod)) + .isInstanceOf(AsyncRequestNotUsableException.class); + } + private HandlerMethod handlerMethod(Object handler, String methodName, Class... paramTypes) throws Exception { Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes); return new InvocableHandlerMethod(handler, method); @@ -296,14 +324,16 @@ public String handle() { } public ResponseEntity> handleWithResponseEntity() { - return new ResponseEntity<>(Collections.singletonMap( - "foo", "bar"), HttpStatus.OK); + return new ResponseEntity<>(Collections.singletonMap("foo", "bar"), HttpStatus.OK); } public ResponseEntity handleBadRequest() { return new ResponseEntity<>("body", HttpStatus.BAD_REQUEST); } + public void handleOutputStream(OutputStream outputStream) throws IOException { + outputStream.write("body".getBytes(StandardCharsets.UTF_8)); + } } From fd76c3358932cac36e8435eb45f5874b1ce91022 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 5 Mar 2024 12:40:32 +0000 Subject: [PATCH 122/261] Fix Javadoc error --- .../web/servlet/view/document/AbstractPdfView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java index 8cac515a6e48..019488099c6e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractPdfView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -138,7 +138,7 @@ protected void prepareWriter(Map model, PdfWriter writer, HttpSe * The subclass can either have fixed preferences or retrieve * them from bean properties defined on the View. * @return an int containing the bits information against PdfWriter definitions - * @see com.lowagie.text.pdf.PdfWriter#AllowPrinting + * @see com.lowagie.text.pdf.PdfWriter#ALLOW_PRINTING * @see com.lowagie.text.pdf.PdfWriter#PageLayoutSinglePage */ protected int getViewerPreferences() { From 7b3fcf2647ee49653be31ac967deb62e2838298f Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 5 Mar 2024 13:06:12 +0000 Subject: [PATCH 123/261] Fix Javadoc error --- .../web/servlet/mvc/support/DefaultHandlerExceptionResolver.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index d39584fd2f11..cb6263464e6f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -452,7 +452,6 @@ protected ModelAndView handleAsyncRequestTimeoutException(AsyncRequestTimeoutExc * @param handler the executed handler, or {@code null} if none chosen * at the time of the exception (for example, if multipart resolution failed) * @return an empty ModelAndView indicating the exception was handled - * @throws IOException potentially thrown from {@link HttpServletResponse#sendError} * @since 5.3.33 */ protected ModelAndView handleAsyncRequestNotUsableException(AsyncRequestNotUsableException ex, From 67ba7dd1da88a27bc484a779c21f705df9832793 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 6 Mar 2024 18:20:31 +0000 Subject: [PATCH 124/261] DisconnectedClientHelper recognizes AsyncRequestNotUsableException See gh-32341 --- .../springframework/web/util/DisconnectedClientHelper.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java index 4ba3441a4726..58da642b8cf7 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -39,7 +39,8 @@ public class DisconnectedClientHelper { Set.of("broken pipe", "connection reset by peer"); private static final Set EXCEPTION_TYPE_NAMES = - Set.of("AbortedException", "ClientAbortException", "EOFException", "EofException"); + Set.of("AbortedException", "ClientAbortException", + "EOFException", "EofException", "AsyncRequestNotUsableException"); private final Log logger; From 36539bdaa93ec8a84874bd6f87dd56d83943fd9c Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 7 Mar 2024 14:47:29 +0000 Subject: [PATCH 125/261] Use wrapped response in HandlerFunctionAdapter webmvc.fn now also uses the StandardServletAsyncWebRequest wrapped response to enforce lifecycle rules from Servlet spec (section 2.3.3.4). See gh-32341 --- .../support/HandlerFunctionAdapter.java | 19 +++- .../support/HandlerFunctionAdapterTests.java | 107 ++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapterTests.java 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 adfea6246d92..d3e59c727766 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -95,6 +95,7 @@ public ModelAndView handle(HttpServletRequest servletRequest, Object handler) throws Exception { WebAsyncManager asyncManager = getWebAsyncManager(servletRequest, servletResponse); + servletResponse = getWrappedResponse(asyncManager); ServerRequest serverRequest = getServerRequest(servletRequest); ServerResponse serverResponse; @@ -124,6 +125,22 @@ private WebAsyncManager getWebAsyncManager(HttpServletRequest servletRequest, Ht return asyncManager; } + /** + * Obtain response wrapped by + * {@link org.springframework.web.context.request.async.StandardServletAsyncWebRequest} + * to enforce lifecycle rules from Servlet spec (section 2.3.3.4) + * in case of async handling. + */ + private static HttpServletResponse getWrappedResponse(WebAsyncManager asyncManager) { + AsyncWebRequest asyncRequest = asyncManager.getAsyncWebRequest(); + Assert.notNull(asyncRequest, "No AsyncWebRequest"); + + HttpServletResponse servletResponse = asyncRequest.getNativeResponse(HttpServletResponse.class); + Assert.notNull(servletResponse, "No HttpServletResponse"); + + return servletResponse; + } + private ServerRequest getServerRequest(HttpServletRequest servletRequest) { ServerRequest serverRequest = (ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapterTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapterTests.java new file mode 100644 index 000000000000..555c45cdddfb --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/HandlerFunctionAdapterTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2024 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.support; + +import java.io.IOException; +import java.util.List; + +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.web.context.request.async.AsyncRequestNotUsableException; +import org.springframework.web.context.request.async.StandardServletAsyncWebRequest; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.testfixture.servlet.MockAsyncContext; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.testfixture.servlet.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.doThrow; +import static org.mockito.BDDMockito.mock; + +/** + * Unit tests for {@link HandlerFunctionAdapter}. + * + * @author Rossen Stoyanchev + */ +public class HandlerFunctionAdapterTests { + + private final MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/"); + + private final MockHttpServletResponse servletResponse = new MockHttpServletResponse(); + + private final HandlerFunctionAdapter adapter = new HandlerFunctionAdapter(); + + + @BeforeEach + void setUp() { + this.servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, + ServerRequest.create(this.servletRequest, List.of(new StringHttpMessageConverter()))); + } + + + @Test + void asyncRequestNotUsable() throws Exception { + + HandlerFunction handler = request -> ServerResponse.sse(sseBuilder -> { + try { + sseBuilder.data("data 1"); + sseBuilder.data("data 2"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + }); + + this.servletRequest.setAsyncSupported(true); + + HttpServletResponse mockServletResponse = mock(HttpServletResponse.class); + doThrow(new IOException("Broken pipe")).when(mockServletResponse).getOutputStream(); + + // Use of response should be rejected + assertThatThrownBy(() -> adapter.handle(servletRequest, mockServletResponse, handler)) + .hasRootCauseInstanceOf(IOException.class) + .hasRootCauseMessage("Broken pipe"); + } + + @Test + void asyncRequestNotUsableOnAsyncDispatch() throws Exception { + + HandlerFunction handler = request -> ServerResponse.ok().body("body"); + + // Put AsyncWebRequest in ERROR state + StandardServletAsyncWebRequest asyncRequest = new StandardServletAsyncWebRequest(servletRequest, servletResponse); + asyncRequest.onError(new AsyncEvent(new MockAsyncContext(servletRequest, servletResponse), new Exception())); + + // Set it as the current AsyncWebRequest, from the initial REQUEST dispatch + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(servletRequest); + asyncManager.setAsyncWebRequest(asyncRequest); + + // Use of response should be rejected + assertThatThrownBy(() -> adapter.handle(servletRequest, servletResponse, handler)) + .isInstanceOf(AsyncRequestNotUsableException.class); + } + +} From ec2c9b5d0e210d62c7a8166a3af7dcd2adaa4115 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 8 Mar 2024 11:33:45 +0100 Subject: [PATCH 126/261] Set error on observation in WebClient instrumentation Prior to this commit, error signals flowing from the client response publisher in `WebClient` would be set on the `Observation.Context`. This is enough for the observation convention to collect data about the error but observation handlers are not notified of this error. This commit sets the error instead on the observation directly to fix this issue. Fixes gh-32399 --- .../web/reactive/function/client/DefaultWebClient.java | 2 +- .../reactive/function/client/WebClientObservationTests.java | 5 +++-- 2 files changed, 4 insertions(+), 3 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 31deb88261da..8c6854143b29 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 @@ -465,7 +465,7 @@ public Mono exchange() { final AtomicBoolean responseReceived = new AtomicBoolean(); return responseMono .doOnNext(response -> responseReceived.set(true)) - .doOnError(observationContext::setError) + .doOnError(observation::error) .doFinally(signalType -> { if (signalType == SignalType.CANCEL && !responseReceived.get()) { observationContext.setAborted(true); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java index 4cd8900ce82a..c3a9460f3c55 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java @@ -110,7 +110,8 @@ void recordsObservationForErrorExchange() { StepVerifier.create(client.get().uri("/path").retrieve().bodyToMono(Void.class)) .expectError(IllegalStateException.class) .verify(Duration.ofSeconds(5)); - assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "IllegalStateException") + assertThatHttpObservation().hasError() + .hasLowCardinalityKeyValue("exception", "IllegalStateException") .hasLowCardinalityKeyValue("status", "CLIENT_ERROR"); } @@ -180,7 +181,7 @@ void recordsObservationWithResponseDetailsWhenFilterFunctionErrors() { StepVerifier.create(responseMono) .expectError(IllegalStateException.class) .verify(Duration.ofSeconds(5)); - assertThatHttpObservation() + assertThatHttpObservation().hasError() .hasLowCardinalityKeyValue("exception", "IllegalStateException") .hasLowCardinalityKeyValue("status", "200"); } From 40d5196243685f82ab284293d09189ee0e0ece72 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 8 Mar 2024 19:31:01 +0100 Subject: [PATCH 127/261] Polishing --- .../modules/ROOT/pages/data-access/orm/jpa.adoc | 10 +++++----- .../beans/factory/support/AbstractBeanFactory.java | 7 ++++--- .../ClassPathBeanDefinitionScannerTests.java | 3 ++- .../PathMatchingResourcePatternResolver.java | 13 ++++++------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc index b9fc4279fc3f..0f843fa8f719 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/jpa.adoc @@ -268,8 +268,8 @@ The actual JPA provider bootstrapping is handed off to the specified executor an running in parallel, to the application bootstrap thread. The exposed `EntityManagerFactory` proxy can be injected into other application components and is even able to respond to `EntityManagerFactoryInfo` configuration inspection. However, once the actual JPA provider -is being accessed by other components (for example, calling `createEntityManager`), those calls -block until the background bootstrapping has completed. In particular, when you use +is being accessed by other components (for example, calling `createEntityManager`), those +calls block until the background bootstrapping has completed. In particular, when you use Spring Data JPA, make sure to set up deferred bootstrapping for its repositories as well. @@ -284,9 +284,9 @@ to a newly created `EntityManager` per operation, in effect making its usage thr It is possible to write code against the plain JPA without any Spring dependencies, by using an injected `EntityManagerFactory` or `EntityManager`. Spring can understand the -`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method level -if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example shows a plain -JPA DAO implementation that uses the `@PersistenceUnit` annotation: +`@PersistenceUnit` and `@PersistenceContext` annotations both at the field and the method +level if a `PersistenceAnnotationBeanPostProcessor` is enabled. The following example +shows a plain JPA DAO implementation that uses the `@PersistenceUnit` annotation: [tabs] ====== 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 5d84e258a141..32a817f17274 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -161,6 +161,9 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp /** Map from scope identifier String to corresponding Scope. */ private final Map scopes = new LinkedHashMap<>(8); + /** Application startup metrics. **/ + private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + /** Map from bean name to merged RootBeanDefinition. */ private final Map mergedBeanDefinitions = new ConcurrentHashMap<>(256); @@ -171,8 +174,6 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp private final ThreadLocal prototypesCurrentlyInCreation = new NamedThreadLocal<>("Prototype beans currently in creation"); - /** Application startup metrics. **/ - private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; /** * Create a new AbstractBeanFactory. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java index e669752de69e..c921e7830a53 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -268,6 +268,7 @@ public void testSimpleScanWithDefaultFiltersAndSpecifiedBeanNameClash() { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setIncludeAnnotationConfig(false); scanner.scan("org.springframework.context.annotation2"); + assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE)) .withMessageContaining("myNamedDao") .withMessageContaining(NamedStubDao.class.getName()) 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 67159494bdc7..4a074e015284 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -557,7 +557,7 @@ protected Resource[] findPathMatchingResources(String locationPattern) throws IO String rootDirPath = determineRootDir(locationPattern); String subPattern = locationPattern.substring(rootDirPath.length()); Resource[] rootDirResources = getResources(rootDirPath); - Set result = new LinkedHashSet<>(16); + Set result = new LinkedHashSet<>(64); for (Resource rootDirResource : rootDirResources) { rootDirResource = resolveRootDirResource(rootDirResource); URL rootDirUrl = rootDirResource.getURL(); @@ -706,7 +706,7 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, // The Sun JRE does not return a slash here, but BEA JRockit does. rootEntryPath = rootEntryPath + "/"; } - Set result = new LinkedHashSet<>(8); + Set result = new LinkedHashSet<>(64); for (Enumeration entries = jarFile.entries(); entries.hasMoreElements();) { JarEntry entry = entries.nextElement(); String entryPath = entry.getName(); @@ -756,7 +756,7 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException { protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { - Set result = new LinkedHashSet<>(); + Set result = new LinkedHashSet<>(64); URI rootDirUri; try { rootDirUri = rootDirResource.getURI(); @@ -865,7 +865,7 @@ protected Set doFindPathMatchingFileResources(Resource rootDirResource * @see PathMatcher#match(String, String) */ protected Set findAllModulePathResources(String locationPattern) throws IOException { - Set result = new LinkedHashSet<>(16); + Set result = new LinkedHashSet<>(64); // Skip scanning the module path when running in a native image. if (NativeDetector.inNativeImage()) { @@ -966,7 +966,7 @@ private static class PatternVirtualFileVisitor implements InvocationHandler { private final String rootPath; - private final Set resources = new LinkedHashSet<>(); + private final Set resources = new LinkedHashSet<>(64); public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) { this.subPattern = subPattern; @@ -997,7 +997,6 @@ else if ("visit".equals(methodName)) { else if ("toString".equals(methodName)) { return toString(); } - throw new IllegalStateException("Unexpected method invocation: " + method); } From 5c8d9cd0b243fea26cf432fcc032e549f497d189 Mon Sep 17 00:00:00 2001 From: ZeroCyan Date: Sun, 10 Mar 2024 16:33:38 +0100 Subject: [PATCH 128/261] Fix order of sections in Validation chapter of reference manual Closes gh-32408 --- framework-docs/modules/ROOT/nav.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index c0aa14fcb7eb..203c88f0624b 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -39,8 +39,8 @@ ** xref:core/resources.adoc[] ** xref:core/validation.adoc[] *** xref:core/validation/validator.adoc[] -*** xref:core/validation/conversion.adoc[] *** xref:core/validation/beans-beans.adoc[] +*** xref:core/validation/conversion.adoc[] *** xref:core/validation/convert.adoc[] *** xref:core/validation/format.adoc[] *** xref:core/validation/format-configuring-formatting-globaldatetimeformat.adoc[] @@ -431,4 +431,4 @@ ** xref:languages/groovy.adoc[] ** xref:languages/dynamic.adoc[] * xref:appendix.adoc[] -* https://github.com/spring-projects/spring-framework/wiki[Wiki] \ No newline at end of file +* https://github.com/spring-projects/spring-framework/wiki[Wiki] From a3647a8c5eb6a6dd41a84e50e32158c25bf1b4f5 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 12 Mar 2024 20:10:01 +0100 Subject: [PATCH 129/261] Polishing (cherry picked from commit 723c94e5ac270188674fb6697bac942d962f4cae) --- ...PathMatchingResourcePatternResolverTests.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java index b234739a270a..30f1ac941f31 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -51,14 +51,14 @@ */ class PathMatchingResourcePatternResolverTests { - private static final String[] CLASSES_IN_CORE_IO_SUPPORT = { "EncodedResource.class", + private static final String[] CLASSES_IN_CORE_IO_SUPPORT = {"EncodedResource.class", "LocalizedResourceHelper.class", "PathMatchingResourcePatternResolver.class", "PropertiesLoaderSupport.class", "PropertiesLoaderUtils.class", "ResourceArrayPropertyEditor.class", "ResourcePatternResolver.class", - "ResourcePatternUtils.class", "SpringFactoriesLoader.class" }; + "ResourcePatternUtils.class", "SpringFactoriesLoader.class"}; - private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = { "PathMatchingResourcePatternResolverTests.class" }; + private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = {"PathMatchingResourcePatternResolverTests.class"}; - private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = { "NonNull.class", "NonNullApi.class", "Nullable.class" }; + private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = {"NonNull.class", "NonNullApi.class", "Nullable.class"}; private PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); @@ -200,7 +200,7 @@ private List getSubPathsIgnoringClassFilesEtc(String pattern, String pat } @Test - void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() { + void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() { Path testResourcesDir = Paths.get("src/test/resources").toAbsolutePath(); String pattern = String.format("file:%s/scanned-resources/**", testResourcesDir); String pathPrefix = ".+?resources/"; @@ -329,8 +329,8 @@ private void assertExactSubPaths(String pattern, String pathPrefix, String... su } private String getPath(Resource resource) { - // Tests fail if we use resouce.getURL().getPath(). They would also fail on Mac OS when - // using resouce.getURI().getPath() if the resource paths are not Unicode normalized. + // Tests fail if we use resource.getURL().getPath(). They would also fail on macOS when + // using resource.getURI().getPath() if the resource paths are not Unicode normalized. // // On the JVM, all tests should pass when using resouce.getFile().getPath(); however, // we use FileSystemResource#getPath since this test class is sometimes run within a From b1fafbf7e19f1905c4c66db76af116ed554aeb53 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 12 Mar 2024 20:26:44 +0100 Subject: [PATCH 130/261] Upgrade to Reactor 2022.0.17 Includes Groovy 4.0.19, OpenPDF 1.3.42, Mockito 5.11 Closes gh-32421 --- framework-platform/framework-platform.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index f8a7623050ee..8992f746c562 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,21 +11,21 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.10.13")) api(platform("io.netty:netty-bom:4.1.107.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.16")) + api(platform("io.projectreactor:reactor-bom:2022.0.17")) api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.18")) + api(platform("org.apache.groovy:groovy-bom:4.0.19")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.eclipse.jetty:jetty-bom:11.0.20")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) api(platform("org.junit:junit-bom:5.9.3")) - api(platform("org.mockito:mockito-bom:5.10.0")) + api(platform("org.mockito:mockito-bom:5.11.0")) constraints { api("com.fasterxml:aalto-xml:1.3.2") api("com.fasterxml.woodstox:woodstox-core:6.5.1") api("com.github.ben-manes.caffeine:caffeine:3.1.8") - api("com.github.librepdf:openpdf:1.3.41") + api("com.github.librepdf:openpdf:1.3.42") api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:jsr305:3.0.2") api("com.google.code.gson:gson:2.10.1") From 08e7f7efa474659a6934d39293d283398327f320 Mon Sep 17 00:00:00 2001 From: Kasper Bisgaard Date: Wed, 13 Mar 2024 10:20:27 +0100 Subject: [PATCH 131/261] Allow UriTemplate to be built with an empty template Closes gh-32437 --- .../org/springframework/web/util/UriTemplate.java | 4 ++-- .../springframework/web/util/UriTemplateTests.java | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java index 770c6ad7498e..94dfb9de617b 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -66,7 +66,7 @@ public class UriTemplate implements Serializable { * @param uriTemplate the URI template string */ public UriTemplate(String uriTemplate) { - Assert.hasText(uriTemplate, "'uriTemplate' must not be null"); + Assert.notNull(uriTemplate, "'uriTemplate' must not be null"); this.uriTemplate = uriTemplate; this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build(); diff --git a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java index 49044594fc20..723aa83a99f0 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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 static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; /** * @author Arjen Poutsma @@ -35,6 +36,16 @@ */ class UriTemplateTests { + @Test + void emptyPathDoesNotThrowException() { + assertThatNoException().isThrownBy(() -> new UriTemplate("")); + } + + @Test + void nullPathThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new UriTemplate(null)); + } + @Test void getVariableNames() { UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); From 072ebb6ffc061f34f8223a065aba28cc65354793 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 13 Mar 2024 18:06:17 +0100 Subject: [PATCH 132/261] Additional unit tests for operations on empty UriTemplate See gh-32432 (cherry picked from commit 54a6d89da78075b09e6cd5c0e55d4a67885fe24a) --- .../web/util/UriTemplateTests.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java index 723aa83a99f0..54c0640bfc07 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java @@ -53,6 +53,13 @@ void getVariableNames() { assertThat(variableNames).as("Invalid variable names").isEqualTo(Arrays.asList("hotel", "booking")); } + @Test + void getVariableNamesFromEmpty() { + UriTemplate template = new UriTemplate(""); + List variableNames = template.getVariableNames(); + assertThat(variableNames).isEmpty(); + } + @Test void expandVarArgs() { UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}"); @@ -60,6 +67,13 @@ void expandVarArgs() { assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42")); } + @Test + void expandVarArgsFromEmpty() { + UriTemplate template = new UriTemplate(""); + URI result = template.expand(); + assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("")); + } + @Test // SPR-9712 void expandVarArgsWithArrayValue() { UriTemplate template = new UriTemplate("/sum?numbers={numbers}"); @@ -135,6 +149,15 @@ void matches() { assertThat(template.matches(null)).as("UriTemplate matches").isFalse(); } + @Test + void matchesAgainstEmpty() { + UriTemplate template = new UriTemplate(""); + assertThat(template.matches("/hotels/1/bookings/42")).as("UriTemplate matches").isFalse(); + assertThat(template.matches("/hotels/bookings")).as("UriTemplate matches").isFalse(); + assertThat(template.matches("")).as("UriTemplate does not match").isTrue(); + assertThat(template.matches(null)).as("UriTemplate matches").isFalse(); + } + @Test void matchesCustomRegex() { UriTemplate template = new UriTemplate("/hotels/{hotel:\\d+}"); @@ -153,6 +176,13 @@ void match() { assertThat(result).as("Invalid match").isEqualTo(expected); } + @Test + void matchAgainstEmpty() { + UriTemplate template = new UriTemplate(""); + Map result = template.match("/hotels/1/bookings/42"); + assertThat(result).as("Invalid match").isEmpty(); + } + @Test void matchCustomRegex() { Map expected = new HashMap<>(2); From f2fd2f12269c6a781c5b2c20b3c24141055a3d68 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 5 Mar 2024 19:16:34 +0000 Subject: [PATCH 133/261] Extract reusable checkSchemeAndPort method Closes gh-32440 --- .../web/util/UriComponentsBuilder.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 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 afea8c3a5397..b05112c65315 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 @@ -77,9 +77,9 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final String HTTP_PATTERN = "(?i)(http|https):"; - private static final String USERINFO_PATTERN = "([^@/?#]*)"; + private static final String USERINFO_PATTERN = "([^/?#]*)"; - private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*"; + private static final String HOST_IPV4_PATTERN = "[^/?#:]*"; private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}:.]*[%\\p{Alnum}]*]"; @@ -252,9 +252,7 @@ public static UriComponentsBuilder fromUriString(String uri) { builder.schemeSpecificPart(ssp); } else { - if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) { - throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL"); - } + checkSchemeAndHost(uri, scheme, host); builder.userInfo(userInfo); builder.host(host); if (StringUtils.hasLength(port)) { @@ -296,9 +294,7 @@ public static UriComponentsBuilder fromHttpUrl(String httpUrl) { builder.scheme(scheme != null ? scheme.toLowerCase() : null); builder.userInfo(matcher.group(4)); String host = matcher.group(5); - if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) { - throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL"); - } + checkSchemeAndHost(httpUrl, scheme, host); builder.host(host); String port = matcher.group(7); if (StringUtils.hasLength(port)) { @@ -317,6 +313,15 @@ public static UriComponentsBuilder fromHttpUrl(String httpUrl) { } } + private static void checkSchemeAndHost(String uri, @Nullable String scheme, @Nullable String host) { + if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) { + throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL"); + } + if (StringUtils.hasLength(host) && host.startsWith("[") && !host.endsWith("]")) { + throw new IllegalArgumentException("Invalid IPV6 host in [" + uri + "]"); + } + } + /** * Create a new {@code UriComponents} object from the URI associated with * the given HttpRequest while also overlaying with values from the headers @@ -402,6 +407,7 @@ public static UriComponentsBuilder fromOriginHeader(String origin) { if (StringUtils.hasLength(port)) { builder.port(port); } + checkSchemeAndHost(origin, scheme, host); return builder; } else { From 6d323d710a35f041dafc6a5294e2ca50d759ed34 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 14 Mar 2024 09:30:16 +0000 Subject: [PATCH 134/261] Next development version (v6.0.19-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 27eecbd21650..83498cea1b3b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.18-SNAPSHOT +version=6.0.19-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 80809bbc14ffe415d4c0faa95ade7a048212c329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 08:52:09 +0100 Subject: [PATCH 135/261] Move CI to GitHub Actions Closes gh-32447 --- .github/actions/send-notification/action.yml | 33 +++++++ .../workflows/build-and-deploy-snapshot.yml | 64 ++++++++++++++ .github/workflows/ci.yml | 80 +++++++++++++++++ .github/workflows/deploy-docs.yml | 10 ++- ci/README.adoc | 2 + ci/parameters.yml | 1 - ci/pipeline.yml | 87 +++---------------- ci/scripts/build-pr.sh | 8 -- ci/scripts/build-project.sh | 9 -- ci/scripts/check-project.sh | 9 -- ci/tasks/build-pr.yml | 19 ---- ci/tasks/build-project.yml | 22 ----- ci/tasks/check-project.yml | 24 ----- 13 files changed, 197 insertions(+), 171 deletions(-) create mode 100644 .github/actions/send-notification/action.yml create mode 100644 .github/workflows/build-and-deploy-snapshot.yml create mode 100644 .github/workflows/ci.yml delete mode 100755 ci/scripts/build-pr.sh delete mode 100755 ci/scripts/build-project.sh delete mode 100755 ci/scripts/check-project.sh delete mode 100644 ci/tasks/build-pr.yml delete mode 100644 ci/tasks/build-project.yml delete mode 100644 ci/tasks/check-project.yml diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml new file mode 100644 index 000000000000..9582d44ed154 --- /dev/null +++ b/.github/actions/send-notification/action.yml @@ -0,0 +1,33 @@ +name: Send notification +description: Sends a Google Chat message as a notification of the job's outcome +inputs: + webhook-url: + description: 'Google Chat Webhook URL' + required: true + status: + description: 'Status of the job' + required: true + build-scan-url: + description: 'URL of the build scan to include in the notification' + run-name: + description: 'Name of the run to include in the notification' + default: ${{ format('{0} {1}', github.ref_name, github.job) }} +runs: + using: composite + steps: + - shell: bash + run: | + echo "BUILD_SCAN=${{ inputs.build-scan-url == '' && ' [build scan unavailable]' || format(' [<{0}|Build Scan>]', inputs.build-scan-url) }}" >> "$GITHUB_ENV" + echo "RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_ENV" + - shell: bash + if: ${{ inputs.status == 'success' }} + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was successful ${{ env.BUILD_SCAN }}"}' || true + - shell: bash + if: ${{ inputs.status == 'failure' }} + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: " *<${{ env.RUN_URL }}|${{ inputs.run-name }}> failed* ${{ env.BUILD_SCAN }}"}' || true + - shell: bash + if: ${{ inputs.status == 'cancelled' }} + run: | + curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was cancelled"}' || true diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml new file mode 100644 index 000000000000..3b15f3196b0d --- /dev/null +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -0,0 +1,64 @@ +name: Build and deploy snapshot +on: + push: + branches: + - 6.0.x +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-deploy-snapshot: + if: ${{ github.repository == 'spring-projects/spring-framework' }} + name: Build and deploy snapshot + runs-on: ubuntu-latest + steps: + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: 17 + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + with: + cache-read-only: false + - name: Configure Gradle properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties + echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties + - name: Build and publish + id: build + env: + CI: 'true' + GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository + - name: Deploy + uses: spring-io/artifactory-deploy-action@v0.0.1 + with: + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + build-name: ${{ format('spring-framework-{0}', github.ref_name)}} + repository: 'libs-snapshot-local' + folder: 'deployment-repository' + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + artifact-properties: | + /**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false + /**/framework-api-*-docs.zip::zip.type=docs + /**/framework-api-*-schema.zip::zip.type=schema + - name: Send notification + uses: ./.github/actions/send-notification + if: always() + with: + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + status: ${{ job.status }} + build-scan-url: ${{ steps.build.outputs.build-scan-url }} + run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000000..fcff95d00ca4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI +on: + push: + branches: + - 6.0.x +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + ci: + if: ${{ github.repository == 'spring-projects/spring-framework' }} + strategy: + matrix: + os: + - id: ubuntu-latest + name: Linux + java: + - version: 17 + toolchain: false + - version: 21 + toolchain: true + exclude: + - os: + name: Linux + java: + version: 17 + name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' + runs-on: ${{ matrix.os.id }} + steps: + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: | + ${{ matrix.java.version }} + ${{ matrix.java.toolchain && '17' || '' }} + - name: Prepare Windows runner + if: ${{ runner.os == 'Windows' }} + run: | + git config --global core.autocrlf true + git config --global core.longPaths true + Stop-Service -name Docker + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + with: + cache-read-only: false + - name: Configure Gradle properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties + echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties + - name: Configure toolchain properties + if: ${{ matrix.java.toolchain }} + shell: bash + run: | + echo toolchainVersion=${{ matrix.java.version }} >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', matrix.java.version) }} >> $HOME/.gradle/gradle.properties + - name: Build + id: build + env: + CI: 'true' + GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + run: ./gradlew check antora + - name: Send notification + uses: ./.github/actions/send-notification + if: always() + with: + webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + status: ${{ job.status }} + build-scan-url: ${{ steps.build.outputs.build-scan-url }} + run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }} \ No newline at end of file diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f3e899c4fe0e..a6309a601bb0 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,12 +1,14 @@ name: Deploy Docs on: push: - branches-ignore: [ gh-pages ] - tags: '**' + branches: + - 'main' + - '*.x' + - '!gh-pages' + tags: + - 'v*' repository_dispatch: types: request-build-reference # legacy - schedule: - - cron: '0 10 * * *' # Once per day at 10am UTC workflow_dispatch: permissions: actions: write diff --git a/ci/README.adoc b/ci/README.adoc index 9ff0d5b1e86c..89780a0b944e 100644 --- a/ci/README.adoc +++ b/ci/README.adoc @@ -1,5 +1,7 @@ == Spring Framework Concourse pipeline +NOTE: CI is being migrated to GitHub Actions. + The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks. The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x[Spring Framework 6.0.x]. diff --git a/ci/parameters.yml b/ci/parameters.yml index 46e3e9b070b5..32cf44f8d5ce 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -8,4 +8,3 @@ milestone: "6.0.x" build-name: "spring-framework" pipeline-name: "spring-framework" concourse-url: "https://ci.spring.io" -task-timeout: 1h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml index ac164eba4503..d0186c65c747 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -23,14 +23,6 @@ anchors: docker-resource-source: &docker-resource-source username: ((docker-hub-username)) password: ((docker-hub-password)) - slack-fail-params: &slack-fail-params - text: > - :concourse-failed: - [$TEXT_FILE_CONTENT] - text_file: git-repo/build/build-scan-uri.txt - silent: true - icon_emoji: ":concourse:" - username: concourse-ci changelog-task-params: &changelog-task-params name: generated-changelog/tag tag: generated-changelog/tag @@ -64,12 +56,6 @@ resource_types: <<: *docker-resource-source repository: dpb587/github-status-resource tag: master -- name: slack-notification - type: registry-image - source: - <<: *docker-resource-source - repository: cfcommunity/slack-notification-resource - tag: latest resources: - name: git-repo type: git @@ -98,19 +84,6 @@ resources: username: ((artifactory-username)) password: ((artifactory-password)) build_name: ((build-name)) -- name: repo-status-build - type: github-status-resource - icon: eye-check-outline - source: - repository: ((github-repo-name)) - access_token: ((github-ci-status-token)) - branch: ((branch)) - context: build -- name: slack-alert - type: slack-notification - icon: slack - source: - url: ((slack-webhook-url)) - name: github-pre-release type: github-release icon: briefcase-download-outline @@ -145,37 +118,23 @@ jobs: - put: ci-image params: image: ci-image/image.tar -- name: build +- name: stage-milestone serial: true - public: true plan: - get: ci-image - get: git-repo - trigger: true - - put: repo-status-build - params: { state: "pending", commit: "git-repo" } - - do: - - task: build-project - image: ci-image - file: git-repo/ci/tasks/build-project.yml - privileged: true - timeout: ((task-timeout)) - params: - <<: *build-project-task-params - on_failure: - do: - - put: repo-status-build - params: { state: "failure", commit: "git-repo" } - - put: slack-alert - params: - <<: *slack-fail-params - - put: repo-status-build - params: { state: "success", commit: "git-repo" } + trigger: false + - task: stage + image: ci-image + file: git-repo/ci/tasks/stage-version.yml + params: + RELEASE_TYPE: M + <<: *gradle-enterprise-task-params - put: artifactory-repo params: &artifactory-params signing_key: ((signing-key)) signing_passphrase: ((signing-passphrase)) - repo: libs-snapshot-local + repo: libs-staging-local folder: distribution-repository build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}" build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}" @@ -200,27 +159,9 @@ jobs: - "/**/framework-docs-*-schema.zip" properties: "zip.type": "schema" - get_params: - threads: 8 -- name: stage-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: M - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - repo: libs-staging-local - - put: git-repo - params: - repository: stage-git-repo + - put: git-repo + params: + repository: stage-git-repo - name: promote-milestone serial: true plan: @@ -262,7 +203,6 @@ jobs: - put: artifactory-repo params: <<: *artifactory-params - repo: libs-staging-local - put: git-repo params: repository: stage-git-repo @@ -307,7 +247,6 @@ jobs: - put: artifactory-repo params: <<: *artifactory-params - repo: libs-staging-local - put: git-repo params: repository: stage-git-repo @@ -352,8 +291,6 @@ jobs: <<: *changelog-task-params groups: -- name: "builds" - jobs: ["build"] - name: "releases" jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] - name: "ci-images" diff --git a/ci/scripts/build-pr.sh b/ci/scripts/build-pr.sh deleted file mode 100755 index 94c4e8df65b4..000000000000 --- a/ci/scripts/build-pr.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check -popd > /dev/null diff --git a/ci/scripts/build-project.sh b/ci/scripts/build-project.sh deleted file mode 100755 index 3844d1a3ddb4..000000000000 --- a/ci/scripts/build-project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository -popd > /dev/null diff --git a/ci/scripts/check-project.sh b/ci/scripts/check-project.sh deleted file mode 100755 index a55cb51a5f43..000000000000 --- a/ci/scripts/check-project.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh - -pushd git-repo > /dev/null -./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17 \ - -PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check antora -popd > /dev/null diff --git a/ci/tasks/build-pr.yml b/ci/tasks/build-pr.yml deleted file mode 100644 index dbf6e9c0cf60..000000000000 --- a/ci/tasks/build-pr.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -platform: linux -inputs: -- name: git-repo -caches: -- path: gradle -params: - BRANCH: - CI: true - GRADLE_ENTERPRISE_ACCESS_KEY: - GRADLE_ENTERPRISE_CACHE_USERNAME: - GRADLE_ENTERPRISE_CACHE_PASSWORD: - GRADLE_ENTERPRISE_URL: https://ge.spring.io -run: - path: bash - args: - - -ec - - | - ${PWD}/git-repo/ci/scripts/build-pr.sh diff --git a/ci/tasks/build-project.yml b/ci/tasks/build-project.yml deleted file mode 100644 index 759749ef433f..000000000000 --- a/ci/tasks/build-project.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -platform: linux -inputs: -- name: git-repo -outputs: -- name: distribution-repository -- name: git-repo -caches: -- path: gradle -params: - BRANCH: - CI: true - GRADLE_ENTERPRISE_ACCESS_KEY: - GRADLE_ENTERPRISE_CACHE_USERNAME: - GRADLE_ENTERPRISE_CACHE_PASSWORD: - GRADLE_ENTERPRISE_URL: https://ge.spring.io -run: - path: bash - args: - - -ec - - | - ${PWD}/git-repo/ci/scripts/build-project.sh diff --git a/ci/tasks/check-project.yml b/ci/tasks/check-project.yml deleted file mode 100644 index bea1185231b9..000000000000 --- a/ci/tasks/check-project.yml +++ /dev/null @@ -1,24 +0,0 @@ ---- -platform: linux -inputs: -- name: git-repo -outputs: -- name: distribution-repository -- name: git-repo -caches: -- path: gradle -params: - BRANCH: - CI: true - MAIN_TOOLCHAIN: - TEST_TOOLCHAIN: - GRADLE_ENTERPRISE_ACCESS_KEY: - GRADLE_ENTERPRISE_CACHE_USERNAME: - GRADLE_ENTERPRISE_CACHE_PASSWORD: - GRADLE_ENTERPRISE_URL: https://ge.spring.io -run: - path: bash - args: - - -ec - - | - ${PWD}/git-repo/ci/scripts/check-project.sh From 769f73ebf19ba9594216aeae2e0524681ce61b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 09:14:22 +0100 Subject: [PATCH 136/261] Polish "Move CI to GitHub Actions" See gh-32447 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fd549f124a3..52cd40bd3bf8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-6.0.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) +# Spring Framework [![Build Status](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=6.0.x)](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3A6.0.x) [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring) This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring". From 658194c1557d23e83f130079745d825ef55df1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 09:15:24 +0100 Subject: [PATCH 137/261] Harmonize Concourse configuration --- ci/images/ci-image/Dockerfile | 2 +- ci/pipeline.yml | 4 ++-- ci/tasks/promote-version.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index bd35ac10e754..18a98b34de84 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:jammy-20240111 +FROM ubuntu:jammy-20240125 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh diff --git a/ci/pipeline.yml b/ci/pipeline.yml index d0186c65c747..385f1f7c4bd9 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -37,7 +37,7 @@ resource_types: source: <<: *docker-resource-source repository: concourse/registry-image-resource - tag: 1.5.0 + tag: 1.8.0 - name: artifactory-resource type: registry-image source: @@ -49,7 +49,7 @@ resource_types: source: <<: *docker-resource-source repository: concourse/github-release-resource - tag: 1.5.5 + tag: 1.8.0 - name: github-status-resource type: registry-image source: diff --git a/ci/tasks/promote-version.yml b/ci/tasks/promote-version.yml index d8fd2dbb374a..515d5b2b977a 100644 --- a/ci/tasks/promote-version.yml +++ b/ci/tasks/promote-version.yml @@ -4,7 +4,7 @@ image_resource: type: registry-image source: repository: springio/concourse-release-scripts - tag: '0.4.0-SNAPSHOT' + tag: '0.4.0' username: ((docker-hub-username)) password: ((docker-hub-password)) inputs: From 547425275ce66cfd6ac143663068512bc420549d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 15 Mar 2024 21:16:37 +0100 Subject: [PATCH 138/261] Consistently apply TaskDecorator to ManagedExecutorService as well Closes gh-32455 --- .../concurrent/ConcurrentTaskExecutor.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java index f0fd31a8abd1..a9033e518229 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -134,11 +134,6 @@ public final Executor getConcurrentExecutor() { * execution callback (which may be a wrapper around the user-supplied task). *

    The primary use case is to set some execution context around the task's * invocation, or to provide some monitoring/statistics for task execution. - *

    NOTE: Exception handling in {@code TaskDecorator} implementations - * is limited to plain {@code Runnable} execution via {@code execute} calls. - * In case of {@code #submit} calls, the exposed {@code Runnable} will be a - * {@code FutureTask} which does not propagate any exceptions; you might - * have to cast it and call {@code Future#get} to evaluate exceptions. * @since 4.3 */ public final void setTaskDecorator(TaskDecorator taskDecorator) { @@ -179,11 +174,10 @@ public ListenableFuture submitListenable(Callable task) { } - private TaskExecutorAdapter getAdaptedExecutor(Executor concurrentExecutor) { - if (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(concurrentExecutor)) { - return new ManagedTaskExecutorAdapter(concurrentExecutor); - } - TaskExecutorAdapter adapter = new TaskExecutorAdapter(concurrentExecutor); + private TaskExecutorAdapter getAdaptedExecutor(Executor originalExecutor) { + TaskExecutorAdapter adapter = + (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(originalExecutor) ? + new ManagedTaskExecutorAdapter(originalExecutor) : new TaskExecutorAdapter(originalExecutor)); if (this.taskDecorator != null) { adapter.setTaskDecorator(this.taskDecorator); } From 19b21b15c16098aa77611412528a9b3946ece68e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 15 Mar 2024 21:17:01 +0100 Subject: [PATCH 139/261] Polishing --- .../scheduling/concurrent/ConcurrentTaskScheduler.java | 3 ++- .../scheduling/concurrent/ThreadPoolTaskScheduler.java | 8 +++++--- .../concurrent/ConcurrentTaskExecutorTests.java | 8 ++++---- .../concurrent/ThreadPoolTaskSchedulerTests.java | 6 +++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java index 9b3e65b33ff7..2131bad09515 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -183,6 +183,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java index bad07b6ee863..c6a1909ed798 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -46,8 +46,9 @@ import org.springframework.util.concurrent.ListenableFutureTask; /** - * Implementation of Spring's {@link TaskScheduler} interface, wrapping - * a native {@link java.util.concurrent.ScheduledThreadPoolExecutor}. + * A standard implementation of Spring's {@link TaskScheduler} interface, wrapping + * a native {@link java.util.concurrent.ScheduledThreadPoolExecutor} and providing + * all applicable configuration options for it. * * @author Juergen Hoeller * @author Mark Fisher @@ -158,6 +159,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { * @see Clock#systemDefaultZone() */ public void setClock(Clock clock) { + Assert.notNull(clock, "Clock must not be null"); this.clock = clock; } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java index 2ffe10450e32..826c351f0193 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,8 +17,8 @@ package org.springframework.scheduling.concurrent; import java.util.concurrent.Executor; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RunnableFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -52,8 +52,8 @@ protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecuto @AfterEach void shutdownExecutor() { for (Runnable task : concurrentExecutor.shutdownNow()) { - if (task instanceof RunnableFuture) { - ((RunnableFuture) task).cancel(true); + if (task instanceof Future) { + ((Future) task).cancel(true); } } } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java index 0bff61ceea3c..64a1144f7fb3 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -101,7 +101,7 @@ void scheduleOneTimeTask() throws Exception { @Test @SuppressWarnings("deprecation") - void scheduleOneTimeFailingTaskWithoutErrorHandler() throws Exception { + void scheduleOneTimeFailingTaskWithoutErrorHandler() { TestTask task = new TestTask(this.testName, 0); Future future = scheduler.schedule(task, new Date()); assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS)); @@ -147,7 +147,7 @@ private void await(CountDownLatch latch) { catch (InterruptedException ex) { throw new IllegalStateException(ex); } - assertThat(latch.getCount()).as("latch did not count down,").isEqualTo(0); + assertThat(latch.getCount()).as("latch did not count down").isEqualTo(0); } From f8926d6a667a288486dc2df9ff88b2c6291faa1e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 16 Mar 2024 14:21:34 +0100 Subject: [PATCH 140/261] Avoid cloning empty Annotation array in TypeDescriptor (backport) Closes gh-32405 --- .../core/convert/TypeDescriptor.java | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 6a2d78d419ad..33face05ed59 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -52,8 +52,6 @@ @SuppressWarnings("serial") public class TypeDescriptor implements Serializable { - private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; - private static final Map, TypeDescriptor> commonTypesCache = new HashMap<>(32); private static final Class[] CACHED_COMMON_TYPES = { @@ -84,7 +82,7 @@ public class TypeDescriptor implements Serializable { public TypeDescriptor(MethodParameter methodParameter) { this.resolvableType = ResolvableType.forMethodParameter(methodParameter); this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType()); - this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ? + this.annotatedElement = AnnotatedElementAdapter.from(methodParameter.getParameterIndex() == -1 ? methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations()); } @@ -96,7 +94,7 @@ public TypeDescriptor(MethodParameter methodParameter) { public TypeDescriptor(Field field) { this.resolvableType = ResolvableType.forField(field); this.type = this.resolvableType.resolve(field.getType()); - this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations()); + this.annotatedElement = AnnotatedElementAdapter.from(field.getAnnotations()); } /** @@ -109,7 +107,7 @@ public TypeDescriptor(Property property) { Assert.notNull(property, "Property must not be null"); this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter()); this.type = this.resolvableType.resolve(property.getType()); - this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations()); + this.annotatedElement = AnnotatedElementAdapter.from(property.getAnnotations()); } /** @@ -125,7 +123,7 @@ public TypeDescriptor(Property property) { public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) { this.resolvableType = resolvableType; this.type = (type != null ? type : resolvableType.toClass()); - this.annotatedElement = new AnnotatedElementAdapter(annotations); + this.annotatedElement = AnnotatedElementAdapter.from(annotations); } @@ -512,12 +510,16 @@ public int hashCode() { public String toString() { StringBuilder builder = new StringBuilder(); for (Annotation ann : getAnnotations()) { - builder.append('@').append(ann.annotationType().getName()).append(' '); + builder.append('@').append(getName(ann.annotationType())).append(' '); } builder.append(getResolvableType()); return builder.toString(); } + private static String getName(Class clazz) { + String canonicalName = clazz.getCanonicalName(); + return (canonicalName != null ? canonicalName : clazz.getName()); + } /** * Create a new type descriptor for an object. @@ -733,15 +735,23 @@ private static TypeDescriptor getRelatedIfResolvable(TypeDescriptor source, Reso * @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class) * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) */ - private class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + private static final class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + + private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]); - @Nullable private final Annotation[] annotations; - public AnnotatedElementAdapter(@Nullable Annotation[] annotations) { + private AnnotatedElementAdapter(Annotation[] annotations) { this.annotations = annotations; } + private static AnnotatedElementAdapter from(@Nullable Annotation[] annotations) { + if (annotations == null || annotations.length == 0) { + return EMPTY; + } + return new AnnotatedElementAdapter(annotations); + } + @Override public boolean isAnnotationPresent(Class annotationClass) { for (Annotation annotation : getAnnotations()) { @@ -766,7 +776,7 @@ public T getAnnotation(Class annotationClass) { @Override public Annotation[] getAnnotations() { - return (this.annotations != null ? this.annotations.clone() : EMPTY_ANNOTATION_ARRAY); + return (isEmpty() ? this.annotations : this.annotations.clone()); } @Override @@ -775,7 +785,7 @@ public Annotation[] getDeclaredAnnotations() { } public boolean isEmpty() { - return ObjectUtils.isEmpty(this.annotations); + return (this.annotations.length == 0); } @Override @@ -791,7 +801,7 @@ public int hashCode() { @Override public String toString() { - return TypeDescriptor.this.toString(); + return "AnnotatedElementAdapter annotations=" + Arrays.toString(this.annotations); } } From c3d186b4d0cf4b5550e1239b88802fbbfd76163a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sat, 16 Mar 2024 14:22:17 +0100 Subject: [PATCH 141/261] Remove superfluous @NonNull declarations --- .../core/metrics/jfr/FlightRecorderStartupStep.java | 6 +++--- .../springframework/mock/web/MockHttpServletRequest.java | 5 ++--- .../test/context/jdbc/SqlScriptsTestExecutionListener.java | 4 +--- .../org/springframework/http/DefaultHttpStatusCode.java | 7 +++---- .../web/testfixture/servlet/MockHttpServletRequest.java | 5 ++--- .../web/servlet/function/DefaultServerRequest.java | 5 ++--- 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java index 915a52833c9d..c3d56b757d1e 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/FlightRecorderStartupStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2024 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,10 +21,10 @@ import java.util.function.Supplier; import org.springframework.core.metrics.StartupStep; -import org.springframework.lang.NonNull; /** * {@link StartupStep} implementation for the Java Flight Recorder. + * *

    This variant delegates to a {@link FlightRecorderStartupEvent JFR event extension} * to collect and record data in Java Flight Recorder. * @@ -114,12 +114,12 @@ public void add(String key, Supplier value) { add(key, value.get()); } - @NonNull @Override public Iterator iterator() { return new TagsIterator(); } + private class TagsIterator implements Iterator { private int idx = 0; diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java index 9acaa1346f42..25032db6a2df 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; @@ -1020,7 +1019,7 @@ public void setCookies(@Nullable Cookie... cookies) { } } - private static String encodeCookies(@NonNull Cookie... cookies) { + private static String encodeCookies(Cookie... cookies) { return Arrays.stream(cookies) .map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue())) .collect(Collectors.joining("; ")); diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index c7e51561f8dd..c269d602ca37 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -35,7 +35,6 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextAnnotationUtils; @@ -315,7 +314,6 @@ else if (logger.isDebugEnabled()) { } } - @NonNull private ResourceDatabasePopulator createDatabasePopulator(MergedSqlConfig mergedSqlConfig) { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding()); diff --git a/spring-web/src/main/java/org/springframework/http/DefaultHttpStatusCode.java b/spring-web/src/main/java/org/springframework/http/DefaultHttpStatusCode.java index cc2f754e8e58..0cf6650939de 100644 --- a/spring-web/src/main/java/org/springframework/http/DefaultHttpStatusCode.java +++ b/spring-web/src/main/java/org/springframework/http/DefaultHttpStatusCode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.io.Serializable; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; /** @@ -80,8 +79,8 @@ private int hundreds() { @Override - public int compareTo(@NonNull HttpStatusCode o) { - return Integer.compare(this.value, o.value()); + public int compareTo(HttpStatusCode other) { + return Integer.compare(this.value, other.value()); } @Override diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java index 33bf23f2bdfe..ed95e535de1a 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; @@ -1020,7 +1019,7 @@ public void setCookies(@Nullable Cookie... cookies) { } } - private static String encodeCookies(@NonNull Cookie... cookies) { + private static String encodeCookies(Cookie... cookies) { return Arrays.stream(cookies) .map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue())) .collect(Collectors.joining("; ")); 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 3a4f4667d5a0..d516c761a1cf 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 @@ -58,7 +58,6 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.RequestPath; import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; @@ -486,7 +485,7 @@ public boolean contains(Object o) { } @Override - public boolean addAll(@NonNull Collection> c) { + public boolean addAll(Collection> c) { throw new UnsupportedOperationException(); } @@ -501,7 +500,7 @@ public boolean removeAll(Collection c) { } @Override - public boolean retainAll(@NonNull Collection c) { + public boolean retainAll(Collection c) { throw new UnsupportedOperationException(); } From 8460a2d285bcfd87170958147f6372877fc8f6ae Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 17 Mar 2024 20:42:29 +0100 Subject: [PATCH 142/261] Restore original toString representation (revert accidental backport) See gh-32405 --- .../org/springframework/core/convert/TypeDescriptor.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index 33face05ed59..d146b9306e6a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -510,16 +510,12 @@ public int hashCode() { public String toString() { StringBuilder builder = new StringBuilder(); for (Annotation ann : getAnnotations()) { - builder.append('@').append(getName(ann.annotationType())).append(' '); + builder.append('@').append(ann.annotationType().getName()).append(' '); } builder.append(getResolvableType()); return builder.toString(); } - private static String getName(Class clazz) { - String canonicalName = clazz.getCanonicalName(); - return (canonicalName != null ? canonicalName : clazz.getName()); - } /** * Create a new type descriptor for an object. @@ -801,7 +797,7 @@ public int hashCode() { @Override public String toString() { - return "AnnotatedElementAdapter annotations=" + Arrays.toString(this.annotations); + return Arrays.toString(this.annotations); } } From ed6c25fb6ecd8942a8b9e62f9db19aa52811038e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:17:04 +0100 Subject: [PATCH 143/261] Avoid unnecessary Annotation array cloning in TypeDescriptor Closes gh-32476 (cherry picked from commit 42a4f2896222959d85ba4642542cfe05aff91f2c) --- .../java/org/springframework/core/convert/TypeDescriptor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index d146b9306e6a..e541ba237bd0 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -750,7 +750,7 @@ private static AnnotatedElementAdapter from(@Nullable Annotation[] annotations) @Override public boolean isAnnotationPresent(Class annotationClass) { - for (Annotation annotation : getAnnotations()) { + for (Annotation annotation : this.annotations) { if (annotation.annotationType() == annotationClass) { return true; } @@ -762,7 +762,7 @@ public boolean isAnnotationPresent(Class annotationClass) @Nullable @SuppressWarnings("unchecked") public T getAnnotation(Class annotationClass) { - for (Annotation annotation : getAnnotations()) { + for (Annotation annotation : this.annotations) { if (annotation.annotationType() == annotationClass) { return (T) annotation; } From 8d745462b473c1fd5b0f2f75f4fffeb2142dace2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 18 Mar 2024 16:03:13 +0100 Subject: [PATCH 144/261] Propagate JMS IllegalStateException from commit/rollbackIfNecessary Closes gh-32473 (cherry picked from commit cd7ba1835cfeb37c0a28f1eba97a9956697b1623) --- .../main/java/org/springframework/jms/support/JmsUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java b/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java index bcbf6fbad18d..1c3c8084b7f8 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -217,7 +217,7 @@ public static void commitIfNecessary(Session session) throws JMSException { try { session.commit(); } - catch (jakarta.jms.TransactionInProgressException | jakarta.jms.IllegalStateException ex) { + catch (jakarta.jms.TransactionInProgressException ex) { // Ignore -> can only happen in case of a JTA transaction. } } @@ -232,7 +232,7 @@ public static void rollbackIfNecessary(Session session) throws JMSException { try { session.rollback(); } - catch (jakarta.jms.TransactionInProgressException | jakarta.jms.IllegalStateException ex) { + catch (jakarta.jms.TransactionInProgressException ex) { // Ignore -> can only happen in case of a JTA transaction. } } From eea00628f9fa61a669164b0d655de5897925b3a2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 18 Mar 2024 16:22:03 +0100 Subject: [PATCH 145/261] Polishing --- .../core/ReactiveTypeDescriptor.java | 8 ++++--- .../springframework/core/ResolvableType.java | 4 ++-- .../springframework/core/codec/Decoder.java | 13 +++++----- .../core/codec/ResourceDecoder.java | 6 +++-- .../support/ObjectToObjectConverter.java | 4 ++-- .../core/io/buffer/DataBufferUtils.java | 24 ++++++++----------- .../metrics/DefaultApplicationStartup.java | 6 +++-- ...AbstractTypeHierarchyTraversingFilter.java | 10 ++++---- 8 files changed, 38 insertions(+), 37 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java index 5e37afabf097..433e2bf3b715 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -98,7 +98,9 @@ public boolean supportsEmpty() { */ public Object getEmptyValue() { Assert.state(this.emptySupplier != null, "Empty values not supported"); - return this.emptySupplier.get(); + Object emptyValue = this.emptySupplier.get(); + Assert.notNull(emptyValue, "Invalid null return value from emptySupplier"); + return emptyValue; } /** @@ -130,7 +132,7 @@ public int hashCode() { /** - * Descriptor for a reactive type that can produce 0..N values. + * Descriptor for a reactive type that can produce {@code 0..N} values. * @param type the reactive type * @param emptySupplier a supplier of an empty-value instance of the reactive type */ diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 7fc4b54bc441..5229e94c5758 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -747,7 +747,7 @@ else if (this.type instanceof ParameterizedType parameterizedType) { * Convenience method that will {@link #getGenerics() get} and * {@link #resolve() resolve} generic parameters. * @return an array of resolved generic parameters (the resulting array - * will never be {@code null}, but it may contain {@code null} elements}) + * will never be {@code null}, but it may contain {@code null} elements) * @see #getGenerics() * @see #resolve() */ diff --git a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java index d37244b764d5..f49063f101ae 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -95,20 +95,19 @@ default T decode(DataBuffer buffer, ResolvableType targetType, @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { CompletableFuture future = decodeToMono(Mono.just(buffer), targetType, mimeType, hints).toFuture(); - Assert.state(future.isDone(), "DataBuffer decoding should have completed."); + Assert.state(future.isDone(), "DataBuffer decoding should have completed"); - Throwable failure; try { return future.get(); } catch (ExecutionException ex) { - failure = ex.getCause(); + Throwable cause = ex.getCause(); + throw (cause instanceof CodecException codecException ? codecException : + new DecodingException("Failed to decode: " + (cause != null ? cause.getMessage() : ex), cause)); } catch (InterruptedException ex) { - failure = ex; + throw new DecodingException("Interrupted during decode", ex); } - throw (failure instanceof CodecException codecException ? codecException : - new DecodingException("Failed to decode: " + failure.getMessage(), failure)); } /** diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java index 87f7ac478ae4..f9d92f5e6e52 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -76,10 +76,11 @@ public Resource decode(DataBuffer dataBuffer, ResolvableType elementType, } Class clazz = elementType.toClass(); - String filename = hints != null ? (String) hints.get(FILENAME_HINT) : null; + String filename = (hints != null ? (String) hints.get(FILENAME_HINT) : null); if (clazz == InputStreamResource.class) { return new InputStreamResource(new ByteArrayInputStream(bytes)) { @Override + @Nullable public String getFilename() { return filename; } @@ -92,6 +93,7 @@ public long contentLength() { else if (Resource.class.isAssignableFrom(clazz)) { return new ByteArrayResource(bytes) { @Override + @Nullable public String getFilename() { return filename; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java index 5b7cccba95f0..b65f11f24f0a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -136,7 +136,7 @@ static boolean hasConversionMethodOrConstructor(Class targetClass, Class s @Nullable private static Executable getValidatedExecutable(Class targetClass, Class sourceClass) { Executable executable = conversionExecutableCache.get(targetClass); - if (isApplicable(executable, sourceClass)) { + if (executable != null && isApplicable(executable, sourceClass)) { return executable; } 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 efc1a8a82770..2099bc568a9c 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,7 +62,7 @@ */ public abstract class DataBufferUtils { - private final static Log logger = LogFactory.getLog(DataBufferUtils.class); + private static final Log logger = LogFactory.getLog(DataBufferUtils.class); private static final Consumer RELEASE_CONSUMER = DataBufferUtils::release; @@ -745,7 +745,7 @@ private interface NestedMatcher extends Matcher { */ private static class SingleByteMatcher implements NestedMatcher { - static SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10}); + static final SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10}); private final byte[] delimiter; @@ -784,7 +784,7 @@ public void reset() { /** * Base class for a {@link NestedMatcher}. */ - private static abstract class AbstractNestedMatcher implements NestedMatcher { + private abstract static class AbstractNestedMatcher implements NestedMatcher { private final byte[] delimiter; @@ -990,7 +990,7 @@ private void read() { DataBuffer.ByteBufferIterator iterator = dataBuffer.writableByteBuffers(); Assert.state(iterator.hasNext(), "No ByteBuffer available"); ByteBuffer byteBuffer = iterator.next(); - Attachment attachment = new Attachment(dataBuffer, iterator); + Attachment attachment = new Attachment(dataBuffer, iterator); this.channel.read(byteBuffer, this.position.get(), attachment, this); } @@ -999,7 +999,7 @@ public void completed(Integer read, Attachment attachment) { attachment.iterator().close(); DataBuffer dataBuffer = attachment.dataBuffer(); - if (this.state.get().equals(State.DISPOSED)) { + if (this.state.get() == State.DISPOSED) { release(dataBuffer); closeChannel(this.channel); return; @@ -1030,13 +1030,13 @@ public void completed(Integer read, Attachment attachment) { } @Override - public void failed(Throwable exc, Attachment attachment) { + public void failed(Throwable ex, Attachment attachment) { attachment.iterator().close(); release(attachment.dataBuffer()); closeChannel(this.channel); this.state.set(State.DISPOSED); - this.sink.error(exc); + this.sink.error(ex); } private enum State { @@ -1095,7 +1095,6 @@ protected void hookOnComplete() { public Context currentContext() { return Context.of(this.sink.contextView()); } - } @@ -1190,13 +1189,13 @@ else if (this.completed.get()) { } @Override - public void failed(Throwable exc, Attachment attachment) { + public void failed(Throwable ex, Attachment attachment) { attachment.iterator().close(); this.sink.next(attachment.dataBuffer()); this.writing.set(false); - this.sink.error(exc); + this.sink.error(ex); } @Override @@ -1205,9 +1204,6 @@ public Context currentContext() { } private record Attachment(ByteBuffer byteBuffer, DataBuffer dataBuffer, DataBuffer.ByteBufferIterator iterator) {} - - } - } diff --git a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java index 9fdb24ac9ed3..9a497d3eb9ee 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,6 +20,8 @@ import java.util.Iterator; import java.util.function.Supplier; +import org.springframework.lang.Nullable; + /** * Default "no op" {@code ApplicationStartup} implementation. * @@ -52,6 +54,7 @@ public long getId() { } @Override + @Nullable public Long getParentId() { return null; } @@ -73,7 +76,6 @@ public StartupStep tag(String key, Supplier value) { @Override public void end() { - } diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java index 96874edb2eac..ee7d6d5046e2 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -73,20 +73,20 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada // Optimization to avoid creating ClassReader for superclass. Boolean superClassMatch = matchSuperClass(superClassName); if (superClassMatch != null) { - if (superClassMatch.booleanValue()) { + if (superClassMatch) { return true; } } else { // Need to read superclass to determine a match... try { - if (match(metadata.getSuperClassName(), metadataReaderFactory)) { + if (match(superClassName, metadataReaderFactory)) { return true; } } catch (IOException ex) { if (logger.isDebugEnabled()) { - logger.debug("Could not read superclass [" + metadata.getSuperClassName() + + logger.debug("Could not read superclass [" + superClassName + "] of type-filtered class [" + metadata.getClassName() + "]"); } } @@ -99,7 +99,7 @@ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metada // Optimization to avoid creating ClassReader for superclass Boolean interfaceMatch = matchInterface(ifc); if (interfaceMatch != null) { - if (interfaceMatch.booleanValue()) { + if (interfaceMatch) { return true; } } From 755968fd2c272e69314de3633b7850384253c8e8 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:19:01 +0100 Subject: [PATCH 146/261] Polishing (cherry picked from commit 836a0b3a40f281a642c122dcfb9976603d692e54) --- ...HeaderContentNegotiationStrategyTests.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java index f14a1fdd6300..08a437f10373 100644 --- a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java +++ b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java @@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Test fixture for HeaderContentNegotiationStrategy tests. + * Tests for {@link HeaderContentNegotiationStrategy}. * * @author Rossen Stoyanchev * @author Juergen Hoeller @@ -49,31 +49,27 @@ public void resolveMediaTypes() throws Exception { this.servletRequest.addHeader("Accept", "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"); List mediaTypes = this.strategy.resolveMediaTypes(this.webRequest); - assertThat(mediaTypes).hasSize(4); - assertThat(mediaTypes.get(0).toString()).isEqualTo("text/html"); - assertThat(mediaTypes.get(1).toString()).isEqualTo("text/x-c"); - assertThat(mediaTypes.get(2).toString()).isEqualTo("text/x-dvi;q=0.8"); - assertThat(mediaTypes.get(3).toString()).isEqualTo("text/plain;q=0.5"); + assertThat(mediaTypes).map(Object::toString) + .containsExactly("text/html", "text/x-c", "text/x-dvi;q=0.8", "text/plain;q=0.5"); } - @Test // SPR-14506 - public void resolveMediaTypesFromMultipleHeaderValues() throws Exception { + @Test // gh-19075 + void resolveMediaTypesFromMultipleHeaderValues() throws Exception { this.servletRequest.addHeader("Accept", "text/plain; q=0.5, text/html"); this.servletRequest.addHeader("Accept", "text/x-dvi; q=0.8, text/x-c"); List mediaTypes = this.strategy.resolveMediaTypes(this.webRequest); - assertThat(mediaTypes).hasSize(4); - assertThat(mediaTypes.get(0).toString()).isEqualTo("text/html"); - assertThat(mediaTypes.get(1).toString()).isEqualTo("text/x-c"); - assertThat(mediaTypes.get(2).toString()).isEqualTo("text/x-dvi;q=0.8"); - assertThat(mediaTypes.get(3).toString()).isEqualTo("text/plain;q=0.5"); + assertThat(mediaTypes).map(Object::toString) + .containsExactly("text/html", "text/x-c", "text/x-dvi;q=0.8", "text/plain;q=0.5"); } @Test public void resolveMediaTypesParseError() throws Exception { this.servletRequest.addHeader("Accept", "textplain; q=0.5"); - assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class).isThrownBy(() -> - this.strategy.resolveMediaTypes(this.webRequest)); + assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class) + .isThrownBy(() -> this.strategy.resolveMediaTypes(this.webRequest)) + .withMessageStartingWith("Could not parse 'Accept' header") + .withMessageContaining("Invalid mime type"); } } From da799bc5192686f5732cec6c52c80c37995233dc Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:20:40 +0100 Subject: [PATCH 147/261] Wrap InvalidMimeTypeException in HttpMediaTypeNotAcceptableException The fix for #31254 resulted in an InvalidMimeTypeException being thrown by MimeTypeUtils.sortBySpecificity() instead of an IllegalArgumentException. However, InvalidMimeTypeException extends IllegalArgumentException. Consequently, the change from IllegalArgumentException to InvalidMimeTypeException did not result in the desired effect in HeaderContentNegotiationStrategy. HeaderContentNegotiationStrategy.resolveMediaTypes() still allows the InvalidMimeTypeException to propagate as-is without wrapping it in an HttpMediaTypeNotAcceptableException. To address this issue, this commit catches InvalidMediaTypeException and InvalidMimeTypeException in HeaderContentNegotiationStrategy and wraps the exception in an HttpMediaTypeNotAcceptableException. See gh-31254 See gh-31769 Closes gh-32483 (cherry picked from commit ef02f0bad87ddd4672c88f9018333e0fb9ba5cde) --- .../HeaderContentNegotiationStrategy.java | 5 +++-- ...HeaderContentNegotiationStrategyTests.java | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java index 9ef86aabfd1a..32bf811d30ca 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -23,6 +23,7 @@ import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.util.CollectionUtils; +import org.springframework.util.InvalidMimeTypeException; import org.springframework.util.MimeTypeUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.context.request.NativeWebRequest; @@ -55,7 +56,7 @@ public List resolveMediaTypes(NativeWebRequest request) MimeTypeUtils.sortBySpecificity(mediaTypes); return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST; } - catch (InvalidMediaTypeException ex) { + catch (InvalidMediaTypeException | InvalidMimeTypeException ex) { throw new HttpMediaTypeNotAcceptableException( "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage()); } diff --git a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java index 08a437f10373..777f29041665 100644 --- a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java +++ b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java @@ -34,6 +34,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sam Brannen */ public class HeaderContentNegotiationStrategyTests { @@ -63,6 +64,27 @@ void resolveMediaTypesFromMultipleHeaderValues() throws Exception { .containsExactly("text/html", "text/x-c", "text/x-dvi;q=0.8", "text/plain;q=0.5"); } + @Test // gh-32483 + void resolveMediaTypesWithMaxElements() throws Exception { + String acceptHeaderValue = "text/plain, text/html,".repeat(25); + this.servletRequest.addHeader("Accept", acceptHeaderValue); + List mediaTypes = this.strategy.resolveMediaTypes(this.webRequest); + + assertThat(mediaTypes).hasSize(50); + assertThat(mediaTypes.stream().map(Object::toString).distinct()) + .containsExactly("text/plain", "text/html"); + } + + @Test // gh-32483 + void resolveMediaTypesWithTooManyElements() { + String acceptHeaderValue = "text/plain,".repeat(51); + this.servletRequest.addHeader("Accept", acceptHeaderValue); + assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class) + .isThrownBy(() -> this.strategy.resolveMediaTypes(this.webRequest)) + .withMessageStartingWith("Could not parse 'Accept' header") + .withMessageEndingWith("Too many elements"); + } + @Test public void resolveMediaTypesParseError() throws Exception { this.servletRequest.addHeader("Accept", "textplain; q=0.5"); From afde96c5406129275c701abdbe713bbd134f4035 Mon Sep 17 00:00:00 2001 From: Minsung Oh <62738554+qww1552@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:45:42 +0900 Subject: [PATCH 148/261] Fix link to vavr in the reference guide Closes gh-32495 --- .../pages/data-access/transaction/declarative/rolling-back.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc index b002be989d24..1de4c5af2567 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc @@ -23,7 +23,7 @@ As of Spring Framework 5.2, the default configuration also provides support for Vavr's `Try` method to trigger transaction rollbacks when it returns a 'Failure'. This allows you to handle functional-style errors using Try and have the transaction automatically rolled back in case of a failure. For more information on Vavr's Try, -refer to the [official Vavr documentation](https://www.vavr.io/vavr-docs/#_try). +refer to the https://docs.vavr.io/#_try[official Vavr documentation]. Here's an example of how to use Vavr's Try with a transactional method: [tabs] From 6610ee360e09c21c4952045b2eb1283fe52a9b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 20 Mar 2024 10:15:43 +0100 Subject: [PATCH 149/261] Upgrade actions that use deprecated features --- .github/workflows/backport-bot.yml | 10 ++++++---- .github/workflows/deploy-docs.yml | 4 ++-- .github/workflows/gradle-wrapper-validation.yml | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backport-bot.yml b/.github/workflows/backport-bot.yml index 153c63247652..4d025ece2ceb 100644 --- a/.github/workflows/backport-bot.yml +++ b/.github/workflows/backport-bot.yml @@ -18,11 +18,13 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - name: Check out code + uses: actions/checkout@v4 + - name: Set up Java + uses: actions/setup-java@v4 with: - distribution: 'temurin' - java-version: '17' + distribution: 'liberica' + java-version: 17 - name: Download BackportBot run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar - name: Backport diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index a6309a601bb0..1d66f04806b0 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -17,8 +17,8 @@ jobs: runs-on: ubuntu-latest if: github.repository_owner == 'spring-projects' steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Check out code + uses: actions/checkout@v4 with: ref: docs-build fetch-depth: 1 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index c80a7e5278d0..cf2c086a063c 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -9,5 +9,5 @@ jobs: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 + - uses: actions/checkout@v4 + - uses: gradle/wrapper-validation-action@v2 From 2c531344c60262ca2415ca996510cfcc1a4e37b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 2 Apr 2024 18:01:37 +0200 Subject: [PATCH 150/261] Upgrade to Gradle 8.7 Closes gh-32567 --- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch delta 34118 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 delta 34176 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 SGW6ptSs!X7{rG*o_g?|vpSEZK diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce5cff..b82aa23a4f05 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 46a082120597f8f1aba98622a07308328d4d059c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 5 Apr 2024 15:54:41 +0200 Subject: [PATCH 151/261] Stop linking to Websphere's Javadoc Closes gh-32582 --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3eec601cc83e..d402f8d9f165 100644 --- a/build.gradle +++ b/build.gradle @@ -113,7 +113,6 @@ configure([rootProject] + javaProjects) { project -> "https://docs.oracle.com/en/java/javase/17/docs/api/", "https://jakarta.ee/specifications/platform/9/apidocs/", "https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ and weblogic.* packages - "https://www.ibm.com/docs/api/v1/content/SSEQTP_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/", // com.ibm.* "https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/", // org.jboss.resource.* "https://docs.jboss.org/hibernate/orm/5.6/javadocs/", "https://eclipse.dev/aspectj/doc/released/aspectj5rt-api", From a0bd13ceb1585660599a5ff6648e09c5ec5d5cbd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 8 Apr 2024 10:16:48 +0200 Subject: [PATCH 152/261] Do not extract FactoryBean generic in case of targetType mismatch Closes gh-32489 --- ...ricTypeAwareAutowireCandidateResolver.java | 5 +- .../support/BeanFactoryGenericsTests.java | 819 ++++++++---------- 2 files changed, 387 insertions(+), 437 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java index 3a3c84b11a1a..72ab84d4f8e8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -108,7 +108,8 @@ protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, Dependenc Class resolvedClass = targetType.resolve(); if (resolvedClass != null && FactoryBean.class.isAssignableFrom(resolvedClass)) { Class typeToBeMatched = dependencyType.resolve(); - if (typeToBeMatched != null && !FactoryBean.class.isAssignableFrom(typeToBeMatched)) { + if (typeToBeMatched != null && !FactoryBean.class.isAssignableFrom(typeToBeMatched) && + !typeToBeMatched.isAssignableFrom(resolvedClass)) { targetType = targetType.getGeneric(); if (descriptor.fallbackMatchAllowed()) { // Matching the Class-based type determination for FactoryBean diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java index 0efebed42ac9..2daf769109a2 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,24 +16,23 @@ package org.springframework.beans.factory.support; -import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; -import java.util.AbstractCollection; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.TypedStringValue; @@ -49,11 +48,10 @@ import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.UrlResource; -import org.springframework.core.testfixture.EnabledForTestGroups; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; +import static org.assertj.core.api.Assertions.entry; /** * @author Juergen Hoeller @@ -64,276 +62,241 @@ class BeanFactoryGenericsTests { @Test - void testGenericSetProperty() { + void genericSetProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - rbd.getPropertyValues().add("integerSet", input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.getPropertyValues().add("integerSet", Set.of("4", "5")); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); } @Test - void testGenericListProperty() throws Exception { + void genericListProperty() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - List input = new ArrayList<>(); - input.add("http://localhost:8080"); - input.add("http://localhost:9090"); - rbd.getPropertyValues().add("resourceList", input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + List input = List.of("http://localhost:8080", "http://localhost:9090"); + bd.getPropertyValues().add("resourceList", input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericListPropertyWithAutowiring() throws Exception { + void genericListPropertyWithAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); - RootBeanDefinition rbd = new RootBeanDefinition(GenericIntegerBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); - bf.registerBeanDefinition("genericBean", rbd); - GenericIntegerBean gb = (GenericIntegerBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericIntegerBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + bf.registerBeanDefinition("genericBean", bd); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + GenericIntegerBean gb = (GenericIntegerBean) bf.getBean("genericBean"); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericListPropertyWithInvalidElementType() { + void genericListPropertyWithInvalidElementType() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericIntegerBean.class); - - List input = new ArrayList<>(); - input.add(1); - rbd.getPropertyValues().add("testBeanList", input); - - bf.registerBeanDefinition("genericBean", rbd); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - bf.getBean("genericBean")) - .withMessageContaining("genericBean") - .withMessageContaining("testBeanList[0]") - .withMessageContaining(TestBean.class.getName()) - .withMessageContaining("Integer"); + + RootBeanDefinition bd = new RootBeanDefinition(GenericIntegerBean.class); + bd.getPropertyValues().add("testBeanList", List.of(1)); + bf.registerBeanDefinition("genericBean", bd); + + assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> bf.getBean("genericBean")) + .withMessageContaining("genericBean") + .withMessageContaining("testBeanList[0]") + .withMessageContaining(TestBean.class.getName()) + .withMessageContaining("Integer"); } @Test - void testGenericListPropertyWithOptionalAutowiring() { + void genericListPropertyWithOptionalAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_BY_TYPE); + bf.registerBeanDefinition("genericBean", bd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getResourceList()).isNull(); } @Test - void testGenericMapProperty() { + void genericMapProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getPropertyValues().add("shortMap", input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getPropertyValues().add("shortMap", input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - void testGenericListOfArraysProperty() { + void genericListOfArraysProperty() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - GenericBean gb = (GenericBean) bf.getBean("listOfArrays"); - assertThat(gb.getListOfArrays()).hasSize(1); - String[] array = gb.getListOfArrays().get(0); - assertThat(array).hasSize(2); - assertThat(array[0]).isEqualTo("value1"); - assertThat(array[1]).isEqualTo("value2"); + GenericBean gb = (GenericBean) bf.getBean("listOfArrays"); + assertThat(gb.getListOfArrays()).containsExactly(new String[] {"value1", "value2"}); } - @Test - void testGenericSetConstructor() { + void genericSetConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Set input = Set.of("4", "5"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); } @Test - void testGenericSetConstructorWithAutowiring() { + void genericSetConstructorWithAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("integer1", 4); bf.registerSingleton("integer2", 5); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", bd); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); } @Test - void testGenericSetConstructorWithOptionalAutowiring() { + void genericSetConstructorWithOptionalAutowiring() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", bd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getIntegerSet()).isNull(); } @Test - void testGenericSetListConstructor() throws Exception { + void genericSetListConstructor() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - List input2 = new ArrayList<>(); - input2.add("http://localhost:8080"); - input2.add("http://localhost:9090"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Set input1 = Set.of("4", "5"); + List input2 = List.of("http://localhost:8080", "http://localhost:9090"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericSetListConstructorWithAutowiring() throws Exception { + void genericSetListConstructorWithAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("integer1", 4); bf.registerSingleton("integer2", 5); bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", bd); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericSetListConstructorWithOptionalAutowiring() throws Exception { + void genericSetListConstructorWithOptionalAutowiring() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.registerSingleton("resource1", new UrlResource("http://localhost:8080")); bf.registerSingleton("resource2", new UrlResource("http://localhost:9090")); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("genericBean", bd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getIntegerSet()).isNull(); assertThat(gb.getResourceList()).isNull(); } @Test - void testGenericSetMapConstructor() { + void genericSetMapConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - Map input2 = new HashMap<>(); - input2.put("4", "5"); - input2.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Set input1 = Set.of("4", "5"); + Map input2 = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - void testGenericMapResourceConstructor() throws Exception { + void genericMapResourceConstructor() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList()).containsExactly(new UrlResource("http://localhost:8080")); } @Test - void testGenericMapMapConstructor() { + void genericMapMapConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - - Map input = new HashMap<>(); - input.put("1", "0"); - input.put("2", "3"); - Map input2 = new HashMap<>(); - input2.put("4", "5"); - input2.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input1 = Map.of( + "1", "0", + "2", "3"); + Map input2 = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getShortMap()).isNotSameAs(gb.getPlainMap()); assertThat(gb.getPlainMap()).hasSize(2); assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); @@ -344,19 +307,18 @@ void testGenericMapMapConstructor() { } @Test - void testGenericMapMapConstructorWithSameRefAndConversion() { + void genericMapMapConstructorWithSameRefAndConversion() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Map input = new HashMap<>(); - input.put("1", "0"); - input.put("2", "3"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input = Map.of( + "1", "0", + "2", "3"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap()).isNotSameAs(gb.getPlainMap()); assertThat(gb.getPlainMap()).hasSize(2); assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); @@ -367,19 +329,18 @@ void testGenericMapMapConstructorWithSameRefAndConversion() { } @Test - void testGenericMapMapConstructorWithSameRefAndNoConversion() { + void genericMapMapConstructorWithSameRefAndNoConversion() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); Map input = new HashMap<>(); input.put((short) 1, 0); input.put((short) 2, 3); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap()).isSameAs(gb.getPlainMap()); assertThat(gb.getShortMap()).hasSize(2); assertThat(gb.getShortMap().get(Short.valueOf("1"))).isEqualTo(0); @@ -387,150 +348,128 @@ void testGenericMapMapConstructorWithSameRefAndNoConversion() { } @Test - void testGenericMapWithKeyTypeConstructor() { + void genericMapWithKeyTypeConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getLongMap().get(4L)).isEqualTo("5"); assertThat(gb.getLongMap().get(6L)).isEqualTo("7"); } @Test - void testGenericMapWithCollectionValueConstructor() { + void genericMapWithCollectionValueConstructor() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addPropertyEditorRegistrar(registry -> registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - - Map> input = new HashMap<>(); - HashSet value1 = new HashSet<>(); - value1.add(1); - input.put("1", value1); - ArrayList value2 = new ArrayList<>(); - value2.add(Boolean.TRUE); - input.put("2", value2); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + bf.addPropertyEditorRegistrar(registry -> + registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); - assertThat(gb.getCollectionMap().get(1) instanceof HashSet).isTrue(); - assertThat(gb.getCollectionMap().get(2) instanceof ArrayList).isTrue(); - } + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + Map> input = Map.of( + "1", Set.of(1), + "2", List.of(Boolean.TRUE)); + bd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getCollectionMap().get(1)).isInstanceOf(Set.class); + assertThat(gb.getCollectionMap().get(2)).isInstanceOf(List.class); + } @Test - void testGenericSetFactoryMethod() { + void genericSetFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Set input = Set.of("4", "5"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); } @Test - void testGenericSetListFactoryMethod() throws Exception { + void genericSetListFactoryMethod() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - List input2 = new ArrayList<>(); - input2.add("http://localhost:8080"); - input2.add("http://localhost:9090"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); - assertThat(gb.getResourceList().get(1)).isEqualTo(new UrlResource("http://localhost:9090")); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Set input1 = Set.of("4", "5"); + List input2 = List.of("http://localhost:8080", "http://localhost:9090"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); + assertThat(gb.getResourceList()) + .containsExactly(new UrlResource("http://localhost:8080"), new UrlResource("http://localhost:9090")); } @Test - void testGenericSetMapFactoryMethod() { + void genericSetMapFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - - Set input = new HashSet<>(); - input.add("4"); - input.add("5"); - Map input2 = new HashMap<>(); - input2.put("4", "5"); - input2.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getIntegerSet().contains(4)).isTrue(); - assertThat(gb.getIntegerSet().contains(5)).isTrue(); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Set input1 = Set.of("4", "5"); + Map input2 = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getIntegerSet()).containsExactlyInAnyOrder(4, 5); assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); } @Test - void testGenericMapResourceFactoryMethod() throws Exception { + void genericMapResourceFactoryMethod() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bd.getConstructorArgumentValues().addGenericArgumentValue("http://localhost:8080"); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); assertThat(gb.getShortMap().get(Short.valueOf("6"))).isEqualTo(7); - assertThat(gb.getResourceList().get(0)).isEqualTo(new UrlResource("http://localhost:8080")); + assertThat(gb.getResourceList()).containsExactly(new UrlResource("http://localhost:8080")); } @Test - void testGenericMapMapFactoryMethod() { + void genericMapMapFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - - Map input = new HashMap<>(); - input.put("1", "0"); - input.put("2", "3"); - Map input2 = new HashMap<>(); - input2.put("4", "5"); - input2.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input2); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Map input1 = Map.of( + "1", "0", + "2", "3"); + Map input2 = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input1); + bd.getConstructorArgumentValues().addGenericArgumentValue(input2); + bf.registerBeanDefinition("genericBean", bd); + + GenericBean gb = (GenericBean) bf.getBean("genericBean"); assertThat(gb.getPlainMap().get("1")).isEqualTo("0"); assertThat(gb.getPlainMap().get("2")).isEqualTo("3"); assertThat(gb.getShortMap().get(Short.valueOf("4"))).isEqualTo(5); @@ -538,109 +477,104 @@ void testGenericMapMapFactoryMethod() { } @Test - void testGenericMapWithKeyTypeFactoryMethod() { + void genericMapWithKeyTypeFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - Map input = new HashMap<>(); - input.put("4", "5"); - input.put("6", "7"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Map input = Map.of( + "4", "5", + "6", "7"); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - bf.registerBeanDefinition("genericBean", rbd); GenericBean gb = (GenericBean) bf.getBean("genericBean"); - assertThat(gb.getLongMap().get(Long.valueOf("4"))).isEqualTo("5"); assertThat(gb.getLongMap().get(Long.valueOf("6"))).isEqualTo("7"); } @Test - void testGenericMapWithCollectionValueFactoryMethod() { + void genericMapWithCollectionValueFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.addPropertyEditorRegistrar(registry -> registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); - RootBeanDefinition rbd = new RootBeanDefinition(GenericBean.class); - rbd.setFactoryMethodName("createInstance"); - - Map> input = new HashMap<>(); - HashSet value1 = new HashSet<>(); - value1.add(1); - input.put("1", value1); - ArrayList value2 = new ArrayList<>(); - value2.add(Boolean.TRUE); - input.put("2", value2); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); - rbd.getConstructorArgumentValues().addGenericArgumentValue(input); - - bf.registerBeanDefinition("genericBean", rbd); - GenericBean gb = (GenericBean) bf.getBean("genericBean"); + bf.addPropertyEditorRegistrar(registry -> + registry.registerCustomEditor(Number.class, new CustomNumberEditor(Integer.class, false))); + + RootBeanDefinition bd = new RootBeanDefinition(GenericBean.class); + bd.setFactoryMethodName("createInstance"); + Map> input = Map.of( + "1", Set.of(1), + "2", List.of(Boolean.TRUE)); + bd.getConstructorArgumentValues().addGenericArgumentValue(Boolean.TRUE); + bd.getConstructorArgumentValues().addGenericArgumentValue(input); + bf.registerBeanDefinition("genericBean", bd); - assertThat(gb.getCollectionMap().get(1) instanceof HashSet).isTrue(); - assertThat(gb.getCollectionMap().get(2) instanceof ArrayList).isTrue(); + GenericBean gb = (GenericBean) bf.getBean("genericBean"); + assertThat(gb.getCollectionMap().get(1)).isInstanceOf(Set.class); + assertThat(gb.getCollectionMap().get(2)).isInstanceOf(List.class); } @Test - void testGenericListBean() throws Exception { + void genericListBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - List list = (List) bf.getBean("list"); - assertThat(list).hasSize(1); - assertThat(list.get(0)).isEqualTo(new URL("http://localhost:8080")); + + NamedUrlList list = bf.getBean("list", NamedUrlList.class); + assertThat(list).containsExactly(new URL("http://localhost:8080")); } @Test - void testGenericSetBean() throws Exception { + void genericSetBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - Set set = (Set) bf.getBean("set"); - assertThat(set).hasSize(1); - assertThat(set.iterator().next()).isEqualTo(new URL("http://localhost:8080")); + + NamedUrlSet set = bf.getBean("set", NamedUrlSet.class); + assertThat(set).containsExactly(new URL("http://localhost:8080")); } @Test - void testGenericMapBean() throws Exception { + void genericMapBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - Map map = (Map) bf.getBean("map"); - assertThat(map).hasSize(1); - assertThat(map.keySet().iterator().next()).isEqualTo(10); - assertThat(map.values().iterator().next()).isEqualTo(new URL("http://localhost:8080")); + + NamedUrlMap map = bf.getBean("map", NamedUrlMap.class); + assertThat(map).containsExactly(entry(10, new URL("http://localhost:8080"))); } @Test - void testGenericallyTypedIntegerBean() { + void genericallyTypedIntegerBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); + GenericIntegerBean gb = (GenericIntegerBean) bf.getBean("integerBean"); assertThat(gb.getGenericProperty()).isEqualTo(10); - assertThat(gb.getGenericListProperty().get(0)).isEqualTo(20); - assertThat(gb.getGenericListProperty().get(1)).isEqualTo(30); + assertThat(gb.getGenericListProperty()).containsExactly(20, 30); } @Test - void testGenericallyTypedSetOfIntegerBean() { + void genericallyTypedSetOfIntegerBean() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); + GenericSetOfIntegerBean gb = (GenericSetOfIntegerBean) bf.getBean("setOfIntegerBean"); - assertThat(gb.getGenericProperty().iterator().next()).isEqualTo(10); - assertThat(gb.getGenericListProperty().get(0).iterator().next()).isEqualTo(20); - assertThat(gb.getGenericListProperty().get(1).iterator().next()).isEqualTo(30); + assertThat(gb.getGenericProperty()).singleElement().isEqualTo(10); + assertThat(gb.getGenericListProperty()).satisfiesExactly( + zero -> assertThat(zero).containsExactly(20), + first -> assertThat(first).containsExactly(30)); } @Test - @EnabledForTestGroups(LONG_RUNNING) - void testSetBean() throws Exception { + void setBean() throws Exception { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); new XmlBeanDefinitionReader(bf).loadBeanDefinitions( new ClassPathResource("genericBeanTests.xml", getClass())); - UrlSet us = (UrlSet) bf.getBean("setBean"); - assertThat(us).hasSize(1); - assertThat(us.iterator().next()).isEqualTo(new URL("https://www.springframework.org")); + + UrlSet urlSet = bf.getBean("setBean", UrlSet.class); + assertThat(urlSet).containsExactly(new URL("https://www.springframework.org")); } /** @@ -655,27 +589,27 @@ void testSetBean() throws Exception { */ @Test void parameterizedStaticFactoryMethod() { - RootBeanDefinition rbd = new RootBeanDefinition(getClass()); - rbd.setFactoryMethodName("createMockitoMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + RootBeanDefinition bd = new RootBeanDefinition(getClass()); + bd.setFactoryMethodName("createMockitoMock"); + bd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); - assertRunnableMockFactory(rbd); + assertRunnableMockFactory(bd); } @Test void parameterizedStaticFactoryMethodWithWrappedClassName() { - RootBeanDefinition rbd = new RootBeanDefinition(); - rbd.setBeanClassName(getClass().getName()); - rbd.setFactoryMethodName("createMockitoMock"); + RootBeanDefinition bd = new RootBeanDefinition(); + bd.setBeanClassName(getClass().getName()); + bd.setFactoryMethodName("createMockitoMock"); // TypedStringValue is used as an equivalent to an XML-defined argument String - rbd.getConstructorArgumentValues().addGenericArgumentValue(new TypedStringValue(Runnable.class.getName())); + bd.getConstructorArgumentValues().addGenericArgumentValue(new TypedStringValue(Runnable.class.getName())); - assertRunnableMockFactory(rbd); + assertRunnableMockFactory(bd); } - private void assertRunnableMockFactory(RootBeanDefinition rbd) { + private void assertRunnableMockFactory(RootBeanDefinition bd) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - bf.registerBeanDefinition("mock", rbd); + bf.registerBeanDefinition("mock", bd); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.getType("mock")).isEqualTo(Runnable.class); @@ -698,14 +632,14 @@ private void assertRunnableMockFactory(RootBeanDefinition rbd) { void parameterizedInstanceFactoryMethod() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition bd2 = new RootBeanDefinition(); + bd2.setFactoryBeanName("mocksControl"); + bd2.setFactoryMethodName("createMock"); + bd2.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + bf.registerBeanDefinition("mock", bd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); @@ -719,14 +653,14 @@ void parameterizedInstanceFactoryMethod() { void parameterizedInstanceFactoryMethodWithNonResolvedClassName() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class.getName()); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition bd2 = new RootBeanDefinition(); + bd2.setFactoryBeanName("mocksControl"); + bd2.setFactoryMethodName("createMock"); + bd2.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class.getName()); + bf.registerBeanDefinition("mock", bd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); @@ -740,14 +674,14 @@ void parameterizedInstanceFactoryMethodWithNonResolvedClassName() { void parameterizedInstanceFactoryMethodWithInvalidClassName() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue("x"); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition rbd2 = new RootBeanDefinition(); + rbd2.setFactoryBeanName("mocksControl"); + rbd2.setFactoryMethodName("createMock"); + rbd2.getConstructorArgumentValues().addGenericArgumentValue("x"); + bf.registerBeanDefinition("mock", rbd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isFalse(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isFalse(); @@ -761,14 +695,14 @@ void parameterizedInstanceFactoryMethodWithInvalidClassName() { void parameterizedInstanceFactoryMethodWithIndexedArgument() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addIndexedArgumentValue(0, Runnable.class); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition bd2 = new RootBeanDefinition(); + bd2.setFactoryBeanName("mocksControl"); + bd2.setFactoryMethodName("createMock"); + bd2.getConstructorArgumentValues().addIndexedArgumentValue(0, Runnable.class); + bf.registerBeanDefinition("mock", bd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); @@ -783,14 +717,14 @@ void parameterizedInstanceFactoryMethodWithTempClassLoader() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setTempClassLoader(new OverridingClassLoader(getClass().getClassLoader())); - RootBeanDefinition rbd = new RootBeanDefinition(MocksControl.class); - bf.registerBeanDefinition("mocksControl", rbd); + RootBeanDefinition bd1 = new RootBeanDefinition(MocksControl.class); + bf.registerBeanDefinition("mocksControl", bd1); - rbd = new RootBeanDefinition(); - rbd.setFactoryBeanName("mocksControl"); - rbd.setFactoryMethodName("createMock"); - rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); - bf.registerBeanDefinition("mock", rbd); + RootBeanDefinition bd2 = new RootBeanDefinition(); + bd2.setFactoryBeanName("mocksControl"); + bd2.setFactoryMethodName("createMock"); + bd2.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class); + bf.registerBeanDefinition("mock", bd2); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); assertThat(bf.isTypeMatch("mock", Runnable.class)).isTrue(); @@ -801,7 +735,7 @@ void parameterizedInstanceFactoryMethodWithTempClassLoader() { } @Test - void testGenericMatchingWithBeanNameDifferentiation() { + void genericMatchingWithBeanNameDifferentiation() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); @@ -817,15 +751,13 @@ void testGenericMatchingWithBeanNameDifferentiation() { String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); String[] doubleStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); String[] floatStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); - assertThat(numberStoreNames).hasSize(2); - assertThat(numberStoreNames[0]).isEqualTo("doubleStore"); - assertThat(numberStoreNames[1]).isEqualTo("floatStore"); + assertThat(numberStoreNames).containsExactly("doubleStore", "floatStore"); assertThat(doubleStoreNames).isEmpty(); assertThat(floatStoreNames).isEmpty(); } @Test - void testGenericMatchingWithFullTypeDifferentiation() { + void genericMatchingWithFullTypeDifferentiation() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); @@ -840,19 +772,17 @@ void testGenericMatchingWithFullTypeDifferentiation() { new RootBeanDefinition(NumberBean.class, RootBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); NumberBean nb = bf.getBean(NumberBean.class); - assertThat(nb.getDoubleStore()).isSameAs(bf.getBean("store1")); - assertThat(nb.getFloatStore()).isSameAs(bf.getBean("store2")); + NumberStore store1 = bf.getBean("store1", NumberStore.class); + assertThat(nb.getDoubleStore()).isSameAs(store1); + NumberStore store2 = bf.getBean("store2", NumberStore.class); + assertThat(nb.getFloatStore()).isSameAs(store2); String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); String[] doubleStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); String[] floatStoreNames = bf.getBeanNamesForType(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); - assertThat(numberStoreNames).hasSize(2); - assertThat(numberStoreNames[0]).isEqualTo("store1"); - assertThat(numberStoreNames[1]).isEqualTo("store2"); - assertThat(doubleStoreNames).hasSize(1); - assertThat(doubleStoreNames[0]).isEqualTo("store1"); - assertThat(floatStoreNames).hasSize(1); - assertThat(floatStoreNames[0]).isEqualTo("store2"); + assertThat(numberStoreNames).containsExactly("store1", "store2"); + assertThat(doubleStoreNames).containsExactly("store1"); + assertThat(floatStoreNames).containsExactly("store2"); ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); ObjectProvider> doubleStoreProvider = bf.getBeanProvider(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); @@ -860,64 +790,40 @@ void testGenericMatchingWithFullTypeDifferentiation() { assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(numberStoreProvider::getObject); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(numberStoreProvider::getIfAvailable); assertThat(numberStoreProvider.getIfUnique()).isNull(); - assertThat(doubleStoreProvider.getObject()).isSameAs(bf.getBean("store1")); - assertThat(doubleStoreProvider.getIfAvailable()).isSameAs(bf.getBean("store1")); - assertThat(doubleStoreProvider.getIfUnique()).isSameAs(bf.getBean("store1")); - assertThat(floatStoreProvider.getObject()).isSameAs(bf.getBean("store2")); - assertThat(floatStoreProvider.getIfAvailable()).isSameAs(bf.getBean("store2")); - assertThat(floatStoreProvider.getIfUnique()).isSameAs(bf.getBean("store2")); + assertThat(doubleStoreProvider.getObject()).isSameAs(store1); + assertThat(doubleStoreProvider.getIfAvailable()).isSameAs(store1); + assertThat(doubleStoreProvider.getIfUnique()).isSameAs(store1); + assertThat(floatStoreProvider.getObject()).isSameAs(store2); + assertThat(floatStoreProvider.getIfAvailable()).isSameAs(store2); + assertThat(floatStoreProvider.getIfUnique()).isSameAs(store2); List> resolved = new ArrayList<>(); for (NumberStore instance : numberStoreProvider) { resolved.add(instance); } - assertThat(resolved).hasSize(2); - assertThat(resolved.get(0)).isSameAs(bf.getBean("store1")); - assertThat(resolved.get(1)).isSameAs(bf.getBean("store2")); - - resolved = numberStoreProvider.stream().toList(); - assertThat(resolved).hasSize(2); - assertThat(resolved.get(0)).isSameAs(bf.getBean("store1")); - assertThat(resolved.get(1)).isSameAs(bf.getBean("store2")); - - resolved = numberStoreProvider.orderedStream().toList(); - assertThat(resolved).hasSize(2); - assertThat(resolved.get(0)).isSameAs(bf.getBean("store2")); - assertThat(resolved.get(1)).isSameAs(bf.getBean("store1")); + assertThat(resolved).containsExactly(store1, store2); + assertThat(numberStoreProvider.stream()).containsExactly(store1, store2); + assertThat(numberStoreProvider.orderedStream()).containsExactly(store2, store1); resolved = new ArrayList<>(); for (NumberStore instance : doubleStoreProvider) { resolved.add(instance); } - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); - - resolved = doubleStoreProvider.stream().collect(Collectors.toList()); - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); - - resolved = doubleStoreProvider.orderedStream().collect(Collectors.toList()); - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store1"))).isTrue(); + assertThat(resolved).containsExactly(store1); + assertThat(doubleStoreProvider.stream()).singleElement().isEqualTo(store1); + assertThat(doubleStoreProvider.orderedStream()).singleElement().isEqualTo(store1); resolved = new ArrayList<>(); for (NumberStore instance : floatStoreProvider) { resolved.add(instance); } - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); - - resolved = floatStoreProvider.stream().collect(Collectors.toList()); - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); - - resolved = floatStoreProvider.orderedStream().collect(Collectors.toList()); - assertThat(resolved).hasSize(1); - assertThat(resolved.contains(bf.getBean("store2"))).isTrue(); + assertThat(resolved).containsExactly(store2); + assertThat(floatStoreProvider.stream()).singleElement().isEqualTo(store2); + assertThat(floatStoreProvider.orderedStream()).singleElement().isEqualTo(store2); } @Test - void testGenericMatchingWithUnresolvedOrderedStream() { + void genericMatchingWithUnresolvedOrderedStream() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); @@ -930,10 +836,25 @@ void testGenericMatchingWithUnresolvedOrderedStream() { bf.registerBeanDefinition("store2", bd2); ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); - List> resolved = numberStoreProvider.orderedStream().toList(); - assertThat(resolved).hasSize(2); - assertThat(resolved.get(0)).isSameAs(bf.getBean("store2")); - assertThat(resolved.get(1)).isSameAs(bf.getBean("store1")); + assertThat(numberStoreProvider.orderedStream()).containsExactly( + bf.getBean("store2", NumberStore.class), bf.getBean("store1", NumberStore.class)); + } + + @Test // gh-32489 + void genericMatchingAgainstFactoryBeanClass() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(MyFactoryBean.class); + // Replicate org.springframework.data.repository.config.RepositoryConfigurationDelegate#registerRepositoriesIn + // behavior of setting targetType, required to hit other branch in + // org.springframework.beans.factory.support.GenericTypeAwareAutowireCandidateResolver.checkGenericTypeMatch + bd.setTargetType(ResolvableType.forClassWithGenerics(MyFactoryBean.class, String.class)); + bf.registerBeanDefinition("myFactoryBean", bd); + bf.registerBeanDefinition("myFactoryBeanHolder", + new RootBeanDefinition(MyFactoryBeanHolder.class, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); + + assertThat(bf.getBean(MyFactoryBeanHolder.class).factoryBeans).contains(bf.getBean(MyFactoryBean.class)); } @@ -999,7 +920,7 @@ public static class MocksControl { @SuppressWarnings("unchecked") public T createMock(Class toMock) { return (T) Proxy.newProxyInstance(BeanFactoryGenericsTests.class.getClassLoader(), new Class[] {toMock}, - (InvocationHandler) (proxy, method, args) -> { + (proxy, method, args) -> { throw new UnsupportedOperationException("mocked!"); }); } @@ -1052,4 +973,32 @@ public static NumberStore newFloatStore() { } } + + public interface MyGenericInterfaceForFactoryBeans { + } + + + public static class MyFactoryBean implements FactoryBean, MyGenericInterfaceForFactoryBeans { + + @Override + public T getObject() { + throw new UnsupportedOperationException(); + } + + @Override + public Class getObjectType() { + return String.class; + } + } + + + public static class MyFactoryBeanHolder { + + List> factoryBeans; // Requested type is not a FactoryBean type + + public MyFactoryBeanHolder(List> factoryBeans) { + this.factoryBeans = factoryBeans; + } + } + } From 9412d782ce1484456939d3a7a93658cf6319c87c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 8 Apr 2024 23:00:33 +0200 Subject: [PATCH 153/261] Revised tests for generic FactoryBean type matching (backported) See gh-32489 --- .../support/BeanFactoryGenericsTests.java | 82 ++++++++++++++++--- ...notationConfigApplicationContextTests.java | 31 ++++--- 2 files changed, 89 insertions(+), 24 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java index 2daf769109a2..d2452cf6fb4e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java @@ -29,6 +29,8 @@ import java.util.Set; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.springframework.beans.factory.BeanCreationException; @@ -756,25 +758,31 @@ void genericMatchingWithBeanNameDifferentiation() { assertThat(floatStoreNames).isEmpty(); } - @Test - void genericMatchingWithFullTypeDifferentiation() { + @ParameterizedTest + @ValueSource(classes = {NumberStoreFactory.class, NumberStoreFactoryBeans.class}) + void genericMatchingWithFullTypeDifferentiation(Class factoryClass) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); - RootBeanDefinition bd1 = new RootBeanDefinition(NumberStoreFactory.class); + RootBeanDefinition bd1 = new RootBeanDefinition(factoryClass); bd1.setFactoryMethodName("newDoubleStore"); bf.registerBeanDefinition("store1", bd1); - RootBeanDefinition bd2 = new RootBeanDefinition(NumberStoreFactory.class); + RootBeanDefinition bd2 = new RootBeanDefinition(factoryClass); bd2.setFactoryMethodName("newFloatStore"); bf.registerBeanDefinition("store2", bd2); - bf.registerBeanDefinition("numberBean", - new RootBeanDefinition(NumberBean.class, RootBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); + RootBeanDefinition bd3 = new RootBeanDefinition(NumberBean.class); + bd3.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); + bd3.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + bf.registerBeanDefinition("numberBean", bd3); - NumberBean nb = bf.getBean(NumberBean.class); NumberStore store1 = bf.getBean("store1", NumberStore.class); - assertThat(nb.getDoubleStore()).isSameAs(store1); NumberStore store2 = bf.getBean("store2", NumberStore.class); + NumberBean nb = bf.getBean(NumberBean.class); + assertThat(nb.getDoubleStore()).isSameAs(store1); + assertThat(nb.getFloatStore()).isSameAs(store2); + nb = bf.getBean(NumberBean.class); + assertThat(nb.getDoubleStore()).isSameAs(store1); assertThat(nb.getFloatStore()).isSameAs(store2); String[] numberStoreNames = bf.getBeanNamesForType(ResolvableType.forClass(NumberStore.class)); @@ -822,16 +830,17 @@ void genericMatchingWithFullTypeDifferentiation() { assertThat(floatStoreProvider.orderedStream()).singleElement().isEqualTo(store2); } - @Test - void genericMatchingWithUnresolvedOrderedStream() { + @ParameterizedTest + @ValueSource(classes = {NumberStoreFactory.class, NumberStoreFactoryBeans.class}) + void genericMatchingWithUnresolvedOrderedStream(Class factoryClass) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); - RootBeanDefinition bd1 = new RootBeanDefinition(NumberStoreFactory.class); + RootBeanDefinition bd1 = new RootBeanDefinition(factoryClass); bd1.setFactoryMethodName("newDoubleStore"); bf.registerBeanDefinition("store1", bd1); - RootBeanDefinition bd2 = new RootBeanDefinition(NumberStoreFactory.class); + RootBeanDefinition bd2 = new RootBeanDefinition(factoryClass); bd2.setFactoryMethodName("newFloatStore"); bf.registerBeanDefinition("store2", bd2); @@ -854,7 +863,22 @@ void genericMatchingAgainstFactoryBeanClass() { bf.registerBeanDefinition("myFactoryBeanHolder", new RootBeanDefinition(MyFactoryBeanHolder.class, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); - assertThat(bf.getBean(MyFactoryBeanHolder.class).factoryBeans).contains(bf.getBean(MyFactoryBean.class)); + assertThat(bf.getBean(MyFactoryBeanHolder.class).factoryBeans).containsOnly(bf.getBean(MyFactoryBean.class)); + } + + @Test // gh-32489 + void genericMatchingAgainstLazyFactoryBeanClass() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + bf.setAutowireCandidateResolver(new GenericTypeAwareAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(MyFactoryBean.class); + bd.setTargetType(ResolvableType.forClassWithGenerics(MyFactoryBean.class, String.class)); + bd.setLazyInit(true); + bf.registerBeanDefinition("myFactoryBean", bd); + bf.registerBeanDefinition("myFactoryBeanHolder", + new RootBeanDefinition(MyFactoryBeanHolder.class, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false)); + + assertThat(bf.getBean(MyFactoryBeanHolder.class).factoryBeans).containsOnly(bf.getBean(MyFactoryBean.class)); } @@ -974,6 +998,38 @@ public static NumberStore newFloatStore() { } + public static class NumberStoreFactoryBeans { + + @Order(1) + public static FactoryBean> newDoubleStore() { + return new FactoryBean<>() { + @Override + public NumberStore getObject() { + return new DoubleStore(); + } + @Override + public Class getObjectType() { + return DoubleStore.class; + } + }; + } + + @Order(0) + public static FactoryBean> newFloatStore() { + return new FactoryBean<>() { + @Override + public NumberStore getObject() { + return new FloatStore(); + } + @Override + public Class getObjectType() { + return FloatStore.class; + } + }; + } + } + + public interface MyGenericInterfaceForFactoryBeans { } 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 1eb25d791ec5..ad81a380accf 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -307,8 +307,8 @@ void individualBeanWithNullReturningSupplier() { 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)); + assertThat(context.getBeansOfType(BeanB.class).values()).singleElement().isSameAs(context.getBean(BeanB.class)); + assertThat(context.getBeansOfType(BeanC.class).values()).singleElement().isSameAs(context.getBean(BeanC.class)); assertThatExceptionOfType(NoSuchBeanDefinitionException.class) .isThrownBy(() -> context.getBeanFactory().resolveNamedBean(BeanA.class)); @@ -409,15 +409,17 @@ void individualBeanWithFactoryBeanTypeAsTargetType() { bd2.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(GenericHolder.class, Integer.class))); bd2.setLazyInit(true); context.registerBeanDefinition("fb2", bd2); - context.registerBeanDefinition("ip", new RootBeanDefinition(FactoryBeanInjectionPoints.class)); + RootBeanDefinition bd3 = new RootBeanDefinition(FactoryBeanInjectionPoints.class); + bd3.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("ip", bd3); context.refresh(); + assertThat(context.getBean("ip", FactoryBeanInjectionPoints.class).factoryBean).isSameAs(context.getBean("&fb1")); + assertThat(context.getBean("ip", FactoryBeanInjectionPoints.class).factoryResult).isSameAs(context.getBean("fb1")); assertThat(context.getType("&fb1")).isEqualTo(GenericHolderFactoryBean.class); assertThat(context.getType("fb1")).isEqualTo(GenericHolder.class); assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(2); assertThat(context.getBeanNamesForType(GenericHolderFactoryBean.class)).hasSize(1); - assertThat(context.getBean("ip", FactoryBeanInjectionPoints.class).factoryBean).isSameAs(context.getBean("&fb1")); - assertThat(context.getBean("ip", FactoryBeanInjectionPoints.class).factoryResult).isSameAs(context.getBean("fb1")); } @Test @@ -425,7 +427,7 @@ void individualBeanWithUnresolvedFactoryBeanTypeAsTargetType() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); RootBeanDefinition bd1 = new RootBeanDefinition(); bd1.setBeanClass(GenericHolderFactoryBean.class); - bd1.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(GenericHolder.class, Object.class))); + bd1.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(GenericHolder.class, String.class))); bd1.setLazyInit(true); context.registerBeanDefinition("fb1", bd1); RootBeanDefinition bd2 = new RootBeanDefinition(); @@ -433,13 +435,17 @@ void individualBeanWithUnresolvedFactoryBeanTypeAsTargetType() { bd2.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(GenericHolder.class, Integer.class))); bd2.setLazyInit(true); context.registerBeanDefinition("fb2", bd2); - context.registerBeanDefinition("ip", new RootBeanDefinition(FactoryResultInjectionPoint.class)); + RootBeanDefinition bd3 = new RootBeanDefinition(FactoryBeanInjectionPoints.class); + bd3.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("ip", bd3); context.refresh(); + assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); + assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); assertThat(context.getType("&fb1")).isEqualTo(GenericHolderFactoryBean.class); assertThat(context.getType("fb1")).isEqualTo(GenericHolder.class); assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(2); - assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); + assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(2); } @Test @@ -453,14 +459,17 @@ void individualBeanWithFactoryBeanObjectTypeAsTargetType() { bd2.setBeanClass(UntypedFactoryBean.class); bd2.setTargetType(ResolvableType.forClassWithGenerics(GenericHolder.class, Integer.class)); context.registerBeanDefinition("fb2", bd2); - context.registerBeanDefinition("ip", new RootBeanDefinition(FactoryResultInjectionPoint.class)); + RootBeanDefinition bd3 = new RootBeanDefinition(FactoryResultInjectionPoint.class); + bd3.setScope(RootBeanDefinition.SCOPE_PROTOTYPE); + context.registerBeanDefinition("ip", bd3); context.refresh(); + assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); + assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); assertThat(context.getType("&fb1")).isEqualTo(GenericHolderFactoryBean.class); assertThat(context.getType("fb1")).isEqualTo(GenericHolder.class); assertThat(context.getBeanNamesForType(FactoryBean.class)).hasSize(2); assertThat(context.getBeanNamesForType(GenericHolderFactoryBean.class)).hasSize(1); - assertThat(context.getBean("ip", FactoryResultInjectionPoint.class).factoryResult).isSameAs(context.getBean("fb1")); } @Test From 2bac1629e634cd137c1b514930ca125ec551a456 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 8 Apr 2024 23:26:19 +0200 Subject: [PATCH 154/261] Upgrade to Groovy 4.0.20, Netty 4.1.108, OpenPDF 1.3.43 --- framework-platform/framework-platform.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 8992f746c562..0645b2733cc0 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,11 +9,11 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.14.3")) api(platform("io.micrometer:micrometer-bom:1.10.13")) - api(platform("io.netty:netty-bom:4.1.107.Final")) + api(platform("io.netty:netty-bom:4.1.108.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2022.0.17")) api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.19")) + api(platform("org.apache.groovy:groovy-bom:4.0.20")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.eclipse.jetty:jetty-bom:11.0.20")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) @@ -25,7 +25,7 @@ dependencies { api("com.fasterxml:aalto-xml:1.3.2") api("com.fasterxml.woodstox:woodstox-core:6.5.1") api("com.github.ben-manes.caffeine:caffeine:3.1.8") - api("com.github.librepdf:openpdf:1.3.42") + api("com.github.librepdf:openpdf:1.3.43") api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:jsr305:3.0.2") api("com.google.code.gson:gson:2.10.1") From aba5f421fcd2d5fc0ad9a1310183e0c4558b2c08 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 8 Apr 2024 23:51:13 +0200 Subject: [PATCH 155/261] Remove accidental backport of 6.1 class --- .../web/util/DisconnectedClientHelper.java | 96 ------------------- 1 file changed, 96 deletions(-) delete mode 100644 spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java diff --git a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java b/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java deleted file mode 100644 index 58da642b8cf7..000000000000 --- a/spring-web/src/main/java/org/springframework/web/util/DisconnectedClientHelper.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2002-2024 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; - -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.core.NestedExceptionUtils; -import org.springframework.util.Assert; - -/** - * Utility methods to assist with identifying and logging exceptions that indicate - * the client has gone away. Such exceptions fill logs with unnecessary stack - * traces. The utility methods help to log a single line message at DEBUG level, - * and a full stacktrace at TRACE level. - * - * @author Rossen Stoyanchev - * @since 6.1 - */ -public class DisconnectedClientHelper { - - private static final Set EXCEPTION_PHRASES = - Set.of("broken pipe", "connection reset by peer"); - - private static final Set EXCEPTION_TYPE_NAMES = - Set.of("AbortedException", "ClientAbortException", - "EOFException", "EofException", "AsyncRequestNotUsableException"); - - private final Log logger; - - - public DisconnectedClientHelper(String logCategory) { - Assert.notNull(logCategory, "'logCategory' is required"); - this.logger = LogFactory.getLog(logCategory); - } - - - /** - * Check via {@link #isClientDisconnectedException} if the exception - * indicates the remote client disconnected, and if so log a single line - * message when DEBUG is on, and a full stacktrace when TRACE is on for - * the configured logger. - */ - public boolean checkAndLogClientDisconnectedException(Throwable ex) { - if (isClientDisconnectedException(ex)) { - if (logger.isTraceEnabled()) { - logger.trace("Looks like the client has gone away", ex); - } - else if (logger.isDebugEnabled()) { - logger.debug("Looks like the client has gone away: " + ex + - " (For a full stack trace, set the log category '" + logger + "' to TRACE level.)"); - } - return true; - } - return false; - } - - /** - * Whether the given exception indicates the client has gone away. - * Known cases covered: - *
        - *
      • ClientAbortException or EOFException for Tomcat - *
      • EofException for Jetty - *
      • IOException "Broken pipe" or "connection reset by peer" - *
      - */ - public static boolean isClientDisconnectedException(Throwable ex) { - String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage(); - if (message != null) { - String text = message.toLowerCase(); - for (String phrase : EXCEPTION_PHRASES) { - if (text.contains(phrase)) { - return true; - } - } - } - return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName()); - } - -} From ba776d7201a82013733bd4d9002fd7f7921bb0f7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 9 Apr 2024 16:14:45 +0200 Subject: [PATCH 156/261] Log column type for limited support message in getResultSetValue Closes gh-32601 (cherry picked from commit c5590ae9e6a3d75feae96f3015a65462c67dbcf8) --- .../org/springframework/jdbc/support/JdbcUtils.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java index 3c6580d0d458..f11434ce6e4e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcUtils.java @@ -229,14 +229,14 @@ else if (obj instanceof Number number) { try { return rs.getObject(index, requiredType); } - catch (AbstractMethodError err) { - logger.debug("JDBC driver does not implement JDBC 4.1 'getObject(int, Class)' method", err); - } - catch (SQLFeatureNotSupportedException ex) { + catch (SQLFeatureNotSupportedException | AbstractMethodError ex) { logger.debug("JDBC driver does not support JDBC 4.1 'getObject(int, Class)' method", ex); } catch (SQLException ex) { - logger.debug("JDBC driver has limited support for JDBC 4.1 'getObject(int, Class)' method", ex); + if (logger.isDebugEnabled()) { + logger.debug("JDBC driver has limited support for 'getObject(int, Class)' with column type: " + + requiredType.getName(), ex); + } } // Corresponding SQL types for JSR-310 / Joda-Time types, left up From 7d178c67712ae312a0ea4ce507fe943b728c0bad Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:41:59 +0200 Subject: [PATCH 157/261] Detect bridge methods across ApplicationContexts in MethodIntrospector Prior to this commit, MethodIntrospector failed to properly detect bridge methods for subsequent invocations of selectMethods() with the same targetType and MetadataLookup, if such subsequent invocations occurred after the ApplicationContext had been refreshed. The reason this occurs is due to the following. - Class#getDeclaredMethods() always returns "child copies" of the underlying Method instances -- which means that `equals()` should be used instead of `==` whenever the compared Method instances can come from different sources (such as the static caches mentioned below). - BridgeMethodResolver caches resolved bridge methods in a static cache -- which is never cleared. - ReflectionUtils caches declared methods in a static cache -- which gets cleared when an ApplicationContext is refreshed. Consequently, if you attempt to load an ApplicationContext twice in the same ClassLoader, the second attempt uses the existing, populated cache for bridged methods but a cleared, empty cache for declared methods. This results in new invocations of Class#getDeclaredMethods(), and identity checks with `==` then fail to detect equivalent bridge methods. This commit addresses this by additionally comparing bridge methods using `equals()` in MethodIntrospector.selectMethods(). Note that the `==` checks remain in place as an optimization for when `equals()` is unnecessary. Closes gh-32586 (cherry picked from commit e702733c7b787300d63c21bc473e1aab5c882991) --- .../core/MethodIntrospector.java | 4 +- .../core/MethodIntrospectorTests.java | 112 ++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java diff --git a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java index 9f905cbda473..f1ceac4a7ccf 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java +++ b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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 @@ * * @author Juergen Hoeller * @author Rossen Stoyanchev + * @author Sam Brannen * @since 4.2.3 */ public final class MethodIntrospector { @@ -75,6 +76,7 @@ public static Map selectMethods(Class targetType, final Metada if (result != null) { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); if (bridgedMethod == specificMethod || bridgedMethod == method || + bridgedMethod.equals(specificMethod) || bridgedMethod.equals(method) || metadataLookup.inspect(bridgedMethod) == null) { methodMap.put(specificMethod, result); } diff --git a/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java b/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java new file mode 100644 index 000000000000..f11f8eb86598 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/MethodIntrospectorTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2024 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.core; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.MethodIntrospector.MetadataLookup; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY; + +/** + * Tests for {@link MethodIntrospector}. + * + * @author Sam Brannen + * @since 5.3.34 + */ +class MethodIntrospectorTests { + + @Test // gh-32586 + void selectMethodsAndClearDeclaredMethodsCacheBetweenInvocations() { + Class targetType = ActualController.class; + + // Preconditions for this use case. + assertThat(targetType).isPublic(); + assertThat(targetType.getSuperclass()).isPackagePrivate(); + + MetadataLookup metadataLookup = (MetadataLookup) method -> { + if (MergedAnnotations.from(method, TYPE_HIERARCHY).isPresent(Mapped.class)) { + return method.getName(); + } + return null; + }; + + // Start with a clean slate. + ReflectionUtils.clearCache(); + + // Round #1 + Map methods = MethodIntrospector.selectMethods(targetType, metadataLookup); + assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete"); + + // Simulate ConfigurableApplicationContext#refresh() which clears the + // ReflectionUtils#declaredMethodsCache but NOT the BridgeMethodResolver#cache. + // As a consequence, ReflectionUtils.getDeclaredMethods(...) will return a + // new set of methods that are logically equivalent to but not identical + // to (in terms of object identity) any bridged methods cached in the + // BridgeMethodResolver cache. + ReflectionUtils.clearCache(); + + // Round #2 + methods = MethodIntrospector.selectMethods(targetType, metadataLookup); + assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete"); + } + + + @Retention(RetentionPolicy.RUNTIME) + @interface Mapped { + } + + interface Controller { + + void unmappedMethod(); + + @Mapped + void update(); + + @Mapped + void delete(); + } + + // Must NOT be public. + abstract static class AbstractController implements Controller { + + @Override + public void unmappedMethod() { + } + + @Override + public void delete() { + } + } + + // MUST be public. + public static class ActualController extends AbstractController { + + @Override + public void update() { + } + } + +} From 88a68ddbd2df5c1b891980551bc522dce22d6563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 9 Apr 2024 19:54:22 +0200 Subject: [PATCH 158/261] Upgrade to Reactor 2022.0.18 Closes gh-32593 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 0645b2733cc0..953b39ad9f92 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.10.13")) api(platform("io.netty:netty-bom:4.1.108.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.17")) + api(platform("io.projectreactor:reactor-bom:2022.0.18")) api(platform("io.rsocket:rsocket-bom:1.1.3")) api(platform("org.apache.groovy:groovy-bom:4.0.20")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From 402246df28048504a709b5c277dfd49116ff5df1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 11 Apr 2024 08:49:11 +0200 Subject: [PATCH 159/261] Refine UriComponentsBuilder parsing This commit refines the expressions for the scheme, user info, host and port parts of the URL in UriComponentsBuilder to better conform to RFC 3986. Fixes gh-32617 --- .../springframework/web/util/UriComponentsBuilder.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 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 b05112c65315..611bb15da434 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 @@ -73,19 +73,19 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?"); - private static final String SCHEME_PATTERN = "([^:/?#]+):"; + private static final String SCHEME_PATTERN = "([^:/?#\\\\]+):"; private static final String HTTP_PATTERN = "(?i)(http|https):"; - private static final String USERINFO_PATTERN = "([^/?#]*)"; + private static final String USERINFO_PATTERN = "([^/?#\\\\]*)"; - private static final String HOST_IPV4_PATTERN = "[^/?#:]*"; + private static final String HOST_IPV4_PATTERN = "[^/?#:\\\\]*"; private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}:.]*[%\\p{Alnum}]*]"; private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")"; - private static final String PORT_PATTERN = "(\\{[^}]+\\}?|[^/?#]*)"; + private static final String PORT_PATTERN = "(\\{[^}]+\\}?|[^/?#\\\\]*)"; private static final String PATH_PATTERN = "([^?#]*)"; From f3bdce455e9e8ae68f9de1dbf1fb66e1fcc14a03 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 11 Apr 2024 07:56:46 +0000 Subject: [PATCH 160/261] Next development version (v6.0.20-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 83498cea1b3b..aeac3c788d62 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.19-SNAPSHOT +version=6.0.20-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 40bf550d565d39d9604194fc0d14be4a85103c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 15 Apr 2024 14:20:48 +0200 Subject: [PATCH 161/261] Ensure multipart data is deleted in WebFlux when connection terminates Before this change temporary files would not consistently be deleted when the connection which uploads the multipart files closes naturally. This change uses the usingWhen Reactor operator to ensure that the termination of the connection doesn't prevent individual file parts from being deleted due to a cancellation signal. See gh-31217 Closes gh-32638 --- .../adapter/DefaultServerWebExchange.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java index 21a3193158ea..2c6f73bfbfcb 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/DefaultServerWebExchange.java @@ -24,7 +24,9 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.stream.Collectors; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.context.ApplicationContext; @@ -249,13 +251,10 @@ public Mono> getMultipartData() { public Mono cleanupMultipart() { return Mono.defer(() -> { if (this.multipartRead) { - return getMultipartData() - .onErrorComplete() - .flatMapIterable(Map::values) - .flatMapIterable(Function.identity()) - .flatMap(part -> part.delete() - .onErrorComplete()) - .then(); + return Mono.usingWhen(getMultipartData().onErrorComplete().map(this::collectParts), + parts -> Mono.empty(), + parts -> Flux.fromIterable(parts).flatMap(part -> part.delete().onErrorComplete()) + ); } else { return Mono.empty(); @@ -263,6 +262,10 @@ public Mono cleanupMultipart() { }); } + private List collectParts(MultiValueMap multipartData) { + return multipartData.values().stream().flatMap(List::stream).collect(Collectors.toList()); + } + @Override public LocaleContext getLocaleContext() { return this.localeContextResolver.resolveLocaleContext(this); From e4ab2aa7756eff3116c1a2f36b8bd83b5dd955b9 Mon Sep 17 00:00:00 2001 From: yhao3 Date: Thu, 18 Apr 2024 12:25:09 +0800 Subject: [PATCH 162/261] Update links to HttpOnly documentation at OWASP in ResponseCookie See gh-32663 Closes gh-32667 (cherry picked from commit 7f27ba3902713e6e6051c81b404dc4b0f41321ca) --- .../main/java/org/springframework/http/ResponseCookie.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 e0b69e0e8736..49e305a5d928 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseCookie.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseCookie.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -110,7 +110,7 @@ public boolean isSecure() { /** * Return {@code true} if the cookie has the "HttpOnly" attribute. - * @see https://www.owasp.org/index.php/HTTPOnly + * @see https://owasp.org/www-community/HttpOnly */ public boolean isHttpOnly() { return this.httpOnly; @@ -268,7 +268,7 @@ public interface ResponseCookieBuilder { /** * Add the "HttpOnly" attribute to the cookie. - * @see https://www.owasp.org/index.php/HTTPOnly + * @see https://owasp.org/www-community/HttpOnly */ ResponseCookieBuilder httpOnly(boolean httpOnly); From 94097fb1129a51bf78b3586ccbe3386629afbaae Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 18 Apr 2024 12:46:55 +0200 Subject: [PATCH 163/261] Polishing (aligned with 6.1.x) --- .../AbstractAutowireCapableBeanFactory.java | 10 +- .../factory/support/AbstractBeanFactory.java | 2 +- ...onWithFactoryBeanEarlyDeductionTests.java} | 98 +++++++++++-------- .../rowset/ResultSetWrappingRowSetTests.java | 59 +++++------ .../DefaultTransactionAttribute.java | 6 +- .../AbstractReactiveTransactionManager.java | 10 +- .../AbstractPlatformTransactionManager.java | 7 +- .../support/DefaultTransactionDefinition.java | 6 +- 8 files changed, 110 insertions(+), 88 deletions(-) rename spring-context/src/test/java/org/springframework/context/annotation/{ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java => ConfigurationWithFactoryBeanEarlyDeductionTests.java} (71%) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index b14b9796d033..c15ff8a81cdb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -818,11 +818,11 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m /** * This implementation attempts to query the FactoryBean's generic parameter metadata * if present to determine the object type. If not present, i.e. the FactoryBean is - * declared as a raw type, checks the FactoryBean's {@code getObjectType} method + * declared as a raw type, it checks the FactoryBean's {@code getObjectType} method * on a plain instance of the FactoryBean, without bean properties applied yet. - * If this doesn't return a type yet, and {@code allowInit} is {@code true} a - * full creation of the FactoryBean is used as fallback (through delegation to the - * superclass's implementation). + * If this doesn't return a type yet and {@code allowInit} is {@code true}, full + * creation of the FactoryBean is attempted as fallback (through delegation to the + * superclass implementation). *

      The shortcut check for a FactoryBean is only applied in case of a singleton * FactoryBean. If the FactoryBean instance itself is not kept as singleton, * it will be fully created to check the type of its exposed object. 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 32a817f17274..5488cc0d73b9 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 @@ -1649,7 +1649,7 @@ protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) { * already. The implementation is allowed to instantiate the target factory bean if * {@code allowInit} is {@code true} and the type cannot be determined another way; * otherwise it is restricted to introspecting signatures and related metadata. - *

      If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} if set on the bean definition + *

      If no {@link FactoryBean#OBJECT_TYPE_ATTRIBUTE} is set on the bean definition * and {@code allowInit} is {@code true}, the default implementation will create * the FactoryBean via {@code getBean} to call its {@code getObjectType} method. * Subclasses are encouraged to optimize this, typically by inspecting the generic diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java similarity index 71% rename from spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java rename to spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java index 74688cd5d81b..28a803f60ad2 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanBeanEarlyDeductionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationWithFactoryBeanEarlyDeductionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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,7 +29,9 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.core.type.AnnotationMetadata; import static org.assertj.core.api.Assertions.assertThat; @@ -39,51 +41,62 @@ * {@link FactoryBean FactoryBeans} defined in the configuration. * * @author Phillip Webb + * @author Juergen Hoeller */ -public class ConfigurationWithFactoryBeanBeanEarlyDeductionTests { +class ConfigurationWithFactoryBeanEarlyDeductionTests { @Test - public void preFreezeDirect() { + void preFreezeDirect() { assertPreFreeze(DirectConfiguration.class); } @Test - public void postFreezeDirect() { + void postFreezeDirect() { assertPostFreeze(DirectConfiguration.class); } @Test - public void preFreezeGenericMethod() { + void preFreezeGenericMethod() { assertPreFreeze(GenericMethodConfiguration.class); } @Test - public void postFreezeGenericMethod() { + void postFreezeGenericMethod() { assertPostFreeze(GenericMethodConfiguration.class); } @Test - public void preFreezeGenericClass() { + void preFreezeGenericClass() { assertPreFreeze(GenericClassConfiguration.class); } @Test - public void postFreezeGenericClass() { + void postFreezeGenericClass() { assertPostFreeze(GenericClassConfiguration.class); } @Test - public void preFreezeAttribute() { + void preFreezeAttribute() { assertPreFreeze(AttributeClassConfiguration.class); } @Test - public void postFreezeAttribute() { + void postFreezeAttribute() { assertPostFreeze(AttributeClassConfiguration.class); } @Test - public void preFreezeUnresolvedGenericFactoryBean() { + void preFreezeTargetType() { + assertPreFreeze(TargetTypeConfiguration.class); + } + + @Test + void postFreezeTargetType() { + assertPostFreeze(TargetTypeConfiguration.class); + } + + @Test + void preFreezeUnresolvedGenericFactoryBean() { // Covers the case where a @Configuration is picked up via component scanning // and its bean definition only has a String bean class. In such cases // beanDefinition.hasBeanClass() returns false so we need to actually @@ -105,14 +118,13 @@ public void preFreezeUnresolvedGenericFactoryBean() { } } + private void assertPostFreeze(Class configurationClass) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - configurationClass); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configurationClass); assertContainsMyBeanName(context); } - private void assertPreFreeze(Class configurationClass, - BeanFactoryPostProcessor... postProcessors) { + private void assertPreFreeze(Class configurationClass, BeanFactoryPostProcessor... postProcessors) { NameCollectingBeanFactoryPostProcessor postProcessor = new NameCollectingBeanFactoryPostProcessor(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); try (context) { @@ -132,41 +144,38 @@ private void assertContainsMyBeanName(String[] names) { assertThat(names).containsExactly("myBean"); } - private static class NameCollectingBeanFactoryPostProcessor - implements BeanFactoryPostProcessor { + + private static class NameCollectingBeanFactoryPostProcessor implements BeanFactoryPostProcessor { private String[] names; @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) - throws BeansException { - this.names = beanFactory.getBeanNamesForType(MyBean.class, true, false); + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + ResolvableType typeToMatch = ResolvableType.forClassWithGenerics(MyBean.class, String.class); + this.names = beanFactory.getBeanNamesForType(typeToMatch, true, false); } public String[] getNames() { return this.names; } - } @Configuration static class DirectConfiguration { @Bean - MyBean myBean() { - return new MyBean(); + MyBean myBean() { + return new MyBean<>(); } - } @Configuration static class GenericMethodConfiguration { @Bean - FactoryBean myBean() { - return new TestFactoryBean<>(new MyBean()); + FactoryBean> myBean() { + return new TestFactoryBean<>(new MyBean<>()); } - } @Configuration @@ -176,13 +185,11 @@ static class GenericClassConfiguration { MyFactoryBean myBean() { return new MyFactoryBean(); } - } @Configuration @Import(AttributeClassRegistrar.class) static class AttributeClassConfiguration { - } static class AttributeClassRegistrar implements ImportBeanDefinitionRegistrar { @@ -191,16 +198,32 @@ static class AttributeClassRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { BeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition( RawWithAbstractObjectTypeFactoryBean.class).getBeanDefinition(); - definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, MyBean.class); + definition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, + ResolvableType.forClassWithGenerics(MyBean.class, String.class)); registry.registerBeanDefinition("myBean", definition); } + } + @Configuration + @Import(TargetTypeRegistrar.class) + static class TargetTypeConfiguration { + } + + static class TargetTypeRegistrar implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + RootBeanDefinition definition = new RootBeanDefinition(RawWithAbstractObjectTypeFactoryBean.class); + definition.setTargetType(ResolvableType.forClassWithGenerics(FactoryBean.class, + ResolvableType.forClassWithGenerics(MyBean.class, String.class))); + registry.registerBeanDefinition("myBean", definition); + } } abstract static class AbstractMyBean { } - static class MyBean extends AbstractMyBean { + static class MyBean extends AbstractMyBean { } static class TestFactoryBean implements FactoryBean { @@ -212,7 +235,7 @@ public TestFactoryBean(T instance) { } @Override - public T getObject() throws Exception { + public T getObject() { return this.instance; } @@ -220,31 +243,26 @@ public T getObject() throws Exception { public Class getObjectType() { return this.instance.getClass(); } - } - static class MyFactoryBean extends TestFactoryBean { + static class MyFactoryBean extends TestFactoryBean> { public MyFactoryBean() { - super(new MyBean()); + super(new MyBean<>()); } - } static class RawWithAbstractObjectTypeFactoryBean implements FactoryBean { - private final Object object = new MyBean(); - @Override public Object getObject() throws Exception { - return object; + throw new IllegalStateException(); } @Override public Class getObjectType() { return MyBean.class; } - } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java index ae58fbf25c34..7ead1f32cc00 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/rowset/ResultSetWrappingRowSetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,167 +37,168 @@ /** * @author Thomas Risberg */ -public class ResultSetWrappingRowSetTests { +class ResultSetWrappingRowSetTests { - private ResultSet resultSet = mock(); + private final ResultSet resultSet = mock(); - private ResultSetWrappingSqlRowSet rowSet = new ResultSetWrappingSqlRowSet(resultSet); + private final ResultSetWrappingSqlRowSet rowSet = new ResultSetWrappingSqlRowSet(resultSet); @Test - public void testGetBigDecimalInt() throws Exception { + void testGetBigDecimalInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getBigDecimal", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBigDecimal", int.class); doTest(rset, rowset, 1, BigDecimal.ONE); } @Test - public void testGetBigDecimalString() throws Exception { + void testGetBigDecimalString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getBigDecimal", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBigDecimal", String.class); doTest(rset, rowset, "test", BigDecimal.ONE); } @Test - public void testGetStringInt() throws Exception { + void testGetStringInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getString", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getString", int.class); doTest(rset, rowset, 1, "test"); } @Test - public void testGetStringString() throws Exception { + void testGetStringString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getString", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getString", String.class); doTest(rset, rowset, "test", "test"); } @Test - public void testGetTimestampInt() throws Exception { + void testGetTimestampInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getTimestamp", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTimestamp", int.class); doTest(rset, rowset, 1, new Timestamp(1234L)); } @Test - public void testGetTimestampString() throws Exception { + void testGetTimestampString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getTimestamp", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTimestamp", String.class); doTest(rset, rowset, "test", new Timestamp(1234L)); } @Test - public void testGetDateInt() throws Exception { + void testGetDateInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getDate", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDate", int.class); doTest(rset, rowset, 1, new Date(1234L)); } @Test - public void testGetDateString() throws Exception { + void testGetDateString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getDate", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDate", String.class); doTest(rset, rowset, "test", new Date(1234L)); } @Test - public void testGetTimeInt() throws Exception { + void testGetTimeInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getTime", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTime", int.class); doTest(rset, rowset, 1, new Time(1234L)); } @Test - public void testGetTimeString() throws Exception { + void testGetTimeString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getTime", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getTime", String.class); doTest(rset, rowset, "test", new Time(1234L)); } @Test - public void testGetObjectInt() throws Exception { + void testGetObjectInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getObject", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getObject", int.class); doTest(rset, rowset, 1, new Object()); } @Test - public void testGetObjectString() throws Exception { + void testGetObjectString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getObject", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getObject", String.class); doTest(rset, rowset, "test", new Object()); } @Test - public void testGetIntInt() throws Exception { + void testGetIntInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getInt", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getInt", int.class); doTest(rset, rowset, 1, 1); } @Test - public void testGetIntString() throws Exception { + void testGetIntString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getInt", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getInt", String.class); doTest(rset, rowset, "test", 1); } @Test - public void testGetFloatInt() throws Exception { + void testGetFloatInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getFloat", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getFloat", int.class); doTest(rset, rowset, 1, 1.0f); } @Test - public void testGetFloatString() throws Exception { + void testGetFloatString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getFloat", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getFloat", String.class); doTest(rset, rowset, "test", 1.0f); } @Test - public void testGetDoubleInt() throws Exception { + void testGetDoubleInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getDouble", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDouble", int.class); doTest(rset, rowset, 1, 1.0d); } @Test - public void testGetDoubleString() throws Exception { + void testGetDoubleString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getDouble", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getDouble", String.class); doTest(rset, rowset, "test", 1.0d); } @Test - public void testGetLongInt() throws Exception { + void testGetLongInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getLong", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getLong", int.class); doTest(rset, rowset, 1, 1L); } @Test - public void testGetLongString() throws Exception { + void testGetLongString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getLong", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getLong", String.class); doTest(rset, rowset, "test", 1L); } @Test - public void testGetBooleanInt() throws Exception { + void testGetBooleanInt() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getBoolean", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBoolean", int.class); doTest(rset, rowset, 1, true); } @Test - public void testGetBooleanString() throws Exception { + void testGetBooleanString() throws Exception { Method rset = ResultSet.class.getDeclaredMethod("getBoolean", int.class); Method rowset = ResultSetWrappingSqlRowSet.class.getDeclaredMethod("getBoolean", String.class); doTest(rset, rowset, "test", true); } + private void doTest(Method rsetMethod, Method rowsetMethod, Object arg, Object ret) throws Exception { if (arg instanceof String) { given(resultSet.findColumn((String) arg)).willReturn(1); @@ -207,9 +208,9 @@ private void doTest(Method rsetMethod, Method rowsetMethod, Object arg, Object r given(rsetMethod.invoke(resultSet, arg)).willReturn(ret).willThrow(new SQLException("test")); } rowsetMethod.invoke(rowSet, arg); - assertThatExceptionOfType(InvocationTargetException.class).isThrownBy(() -> - rowsetMethod.invoke(rowSet, arg)). - satisfies(ex -> assertThat(ex.getTargetException()).isExactlyInstanceOf(InvalidResultSetAccessException.class)); + assertThatExceptionOfType(InvocationTargetException.class) + .isThrownBy(() -> rowsetMethod.invoke(rowSet, arg)) + .satisfies(ex -> assertThat(ex.getTargetException()).isExactlyInstanceOf(InvalidResultSetAccessException.class)); } } diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java index ab3df073c7c6..76e72bcd9d04 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -51,7 +51,7 @@ public class DefaultTransactionAttribute extends DefaultTransactionDefinition im /** - * Create a new DefaultTransactionAttribute, with default settings. + * Create a new {@code DefaultTransactionAttribute} with default settings. * Can be modified through bean property setters. * @see #setPropagationBehavior * @see #setIsolationLevel @@ -76,7 +76,7 @@ public DefaultTransactionAttribute(TransactionAttribute other) { } /** - * Create a new DefaultTransactionAttribute with the given + * Create a new {@code DefaultTransactionAttribute} with the given * propagation behavior. Can be modified through bean property setters. * @param propagationBehavior one of the propagation constants in the * TransactionDefinition interface diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java index ebeeeb5f2018..ee3007aa029e 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/AbstractReactiveTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -210,11 +210,13 @@ private Mono handleExistingTransaction(TransactionSynchroni prepareSynchronization(synchronizationManager, status, definition)).thenReturn(status); } - // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED. + // PROPAGATION_REQUIRED, PROPAGATION_SUPPORTS, PROPAGATION_MANDATORY: + // regular participation in existing transaction. if (debugEnabled) { logger.debug("Participating in existing transaction"); } - return Mono.just(prepareReactiveTransaction(synchronizationManager, definition, transaction, false, debugEnabled, null)); + return Mono.just(prepareReactiveTransaction( + synchronizationManager, definition, transaction, false, debugEnabled, null)); } /** @@ -330,7 +332,7 @@ private Mono resume(TransactionSynchronizationManager synchronizationManag if (resourcesHolder != null) { Object suspendedResources = resourcesHolder.suspendedResources; if (suspendedResources != null) { - resume = doResume(synchronizationManager, transaction, suspendedResources); + resume = doResume(synchronizationManager, transaction, suspendedResources); } List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; if (suspendedSynchronizations != null) { diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java index 3721c8df3f00..5b3a68c71f50 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/AbstractPlatformTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -451,7 +451,7 @@ private TransactionStatus handleExistingTransaction( if (useSavepointForNestedTransaction()) { // Create savepoint within existing Spring-managed transaction, // through the SavepointManager API implemented by TransactionStatus. - // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization. + // Usually uses JDBC savepoints. Never activates Spring synchronization. DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); status.createAndHoldSavepoint(); @@ -465,7 +465,8 @@ private TransactionStatus handleExistingTransaction( } } - // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED. + // PROPAGATION_REQUIRED, PROPAGATION_SUPPORTS, PROPAGATION_MANDATORY: + // regular participation in existing transaction. if (debugEnabled) { logger.debug("Participating in existing transaction"); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/support/DefaultTransactionDefinition.java b/spring-tx/src/main/java/org/springframework/transaction/support/DefaultTransactionDefinition.java index c42a0f8208e4..8cce62d42375 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/support/DefaultTransactionDefinition.java +++ b/spring-tx/src/main/java/org/springframework/transaction/support/DefaultTransactionDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -65,7 +65,7 @@ public class DefaultTransactionDefinition implements TransactionDefinition, Seri /** - * Create a new DefaultTransactionDefinition, with default settings. + * Create a new {@code DefaultTransactionDefinition} with default settings. * Can be modified through bean property setters. * @see #setPropagationBehavior * @see #setIsolationLevel @@ -93,7 +93,7 @@ public DefaultTransactionDefinition(TransactionDefinition other) { } /** - * Create a new DefaultTransactionDefinition with the given + * Create a new {@code DefaultTransactionDefinition} with the given * propagation behavior. Can be modified through bean property setters. * @param propagationBehavior one of the propagation constants in the * TransactionDefinition interface From 190397d5e00c0f55c672caf35408b421837db07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 22 Apr 2024 12:11:30 +0200 Subject: [PATCH 164/261] Upgrade to Java 17.0.11 --- .sdkmanrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.sdkmanrc b/.sdkmanrc index 3c4d046f46e3..d8db3808ef1e 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=17.0.8.1-librca +java=17.0.11-librca From 55f9581743b6c981ec36d394c43444a4d5f9dc93 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 22 Apr 2024 13:43:02 +0200 Subject: [PATCH 165/261] Try early initialization for all user-declared methods (including interfaces) Closes gh-32682 (cherry picked from commit 62efdfb89c368fd3a3665fe78a7180692d174c23) --- .../main/java/org/springframework/cglib/proxy/MethodProxy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java b/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java index ca468ca4324a..de3e7de1b036 100644 --- a/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/MethodProxy.java @@ -57,7 +57,7 @@ public static MethodProxy create(Class c1, Class c2, String desc, String name1, proxy.createInfo = new CreateInfo(c1, c2); // SPRING PATCH BEGIN - if (!c1.isInterface() && c1 != Object.class && !Factory.class.isAssignableFrom(c2)) { + if (c1 != Object.class && c1.isAssignableFrom(c2.getSuperclass()) && !Factory.class.isAssignableFrom(c2)) { // Try early initialization for overridden methods on specifically purposed subclasses try { proxy.init(); From 5c9f36435280395e138cfda590195a402b42ac0e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 22 Apr 2024 13:43:07 +0200 Subject: [PATCH 166/261] Polishing (cherry picked from commit ec1f5ca600d61efa877bb04e67f341af2a14b6b2) --- .../ConfigurationClassEnhancer.java | 4 +-- .../ConfigurationClassPostProcessor.java | 25 ++++++++++--------- ...lassPostProcessorAotContributionTests.java | 20 +++++++-------- .../cglib/core/ReflectUtils.java | 2 +- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 73a922b7cf8f..2521a0f2f5a6 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,7 +225,6 @@ public void end_class() { }; return new TransformingClassGenerator(cg, transformer); } - } @@ -334,6 +333,7 @@ public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName); } + @Nullable private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, ConfigurableBeanFactory beanFactory, String beanName) { 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 fd8d0f92a1ea..4eb4a5809f14 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -386,11 +386,11 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. }); // Detect any custom bean name generation strategy supplied through the enclosing application context - SingletonBeanRegistry sbr = null; - if (registry instanceof SingletonBeanRegistry _sbr) { - sbr = _sbr; + SingletonBeanRegistry singletonRegistry = null; + if (registry instanceof SingletonBeanRegistry sbr) { + singletonRegistry = sbr; if (!this.localBeanNameGeneratorSet) { - BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton( + BeanNameGenerator generator = (BeanNameGenerator) singletonRegistry.getSingleton( AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR); if (generator != null) { this.componentScanBeanNameGenerator = generator; @@ -451,8 +451,8 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. while (!candidates.isEmpty()); // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes - if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { - sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); + if (singletonRegistry != null && !singletonRegistry.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { + singletonRegistry.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); } // Store the PropertySourceDescriptors to contribute them Ahead-of-time if necessary @@ -550,6 +550,7 @@ public ImportAwareBeanPostProcessor(BeanFactory beanFactory) { } @Override + @Nullable public PropertyValues postProcessProperties(@Nullable PropertyValues pvs, Object bean, String beanName) { // Inject the BeanFactory before AutowiredAnnotationBeanPostProcessor's // postProcessProperties method attempts to autowire other configuration beans. @@ -645,9 +646,9 @@ private Map buildImportAwareMappings() { } return mappings; } - } + private static class PropertySourcesAotContribution implements BeanFactoryInitializationAotContribution { private static final String ENVIRONMENT_VARIABLE = "environment"; @@ -743,15 +744,14 @@ private CodeBlock handleNull(@Nullable Object value, Supplier nonNull return nonNull.get(); } } - } + private static class ConfigurationClassProxyBeanRegistrationCodeFragments extends BeanRegistrationCodeFragmentsDecorator { private final Class proxyClass; - public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments, - Class proxyClass) { + public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCodeFragments codeFragments, Class proxyClass) { super(codeFragments); this.proxyClass = proxyClass; } @@ -759,6 +759,7 @@ public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCode @Override public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate attributeFilter) { + CodeBlock.Builder code = CodeBlock.builder(); code.add(super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, beanDefinition, attributeFilter)); @@ -771,6 +772,7 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut) { + Executable executableToUse = proxyExecutable(generationContext.getRuntimeHints(), constructorOrFactoryMethod); return super.generateInstanceSupplierCode(generationContext, beanRegistrationCode, executableToUse, allowDirectSupplierShortcut); @@ -788,7 +790,6 @@ private Executable proxyExecutable(RuntimeHints runtimeHints, Executable userExe } return userExecutable; } - } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java index 13cb21a2f39e..7f16ece2833f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -99,8 +99,8 @@ void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorWithMapEntr initializer.accept(freshBeanFactory); freshContext.refresh(); assertThat(freshBeanFactory.getBeanPostProcessors()).filteredOn(ImportAwareAotBeanPostProcessor.class::isInstance) - .singleElement().satisfies(postProcessor -> assertPostProcessorEntry(postProcessor, ImportAwareConfiguration.class, - ImportConfiguration.class)); + .singleElement().satisfies(postProcessor -> + assertPostProcessorEntry(postProcessor, ImportAwareConfiguration.class, ImportConfiguration.class)); freshContext.close(); }); } @@ -117,8 +117,8 @@ void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorAfterApplic freshContext.refresh(); TestAwareCallbackBean bean = freshContext.getBean(TestAwareCallbackBean.class); assertThat(bean.instances).hasSize(2); - assertThat(bean.instances.get(0)).isEqualTo(freshContext); - assertThat(bean.instances.get(1)).isInstanceOfSatisfying(AnnotationMetadata.class, metadata -> + assertThat(bean.instances).element(0).isEqualTo(freshContext); + assertThat(bean.instances).element(1).isInstanceOfSatisfying(AnnotationMetadata.class, metadata -> assertThat(metadata.getClassName()).isEqualTo(TestAwareCallbackConfiguration.class.getName())); freshContext.close(); }); @@ -236,13 +236,14 @@ public int getOrder() { } @Override - public void afterPropertiesSet() throws Exception { + public void afterPropertiesSet() { Assert.notNull(this.metadata, "Metadata was not injected"); } } } + @Nested class PropertySourceTests { @@ -362,9 +363,9 @@ static class PropertySourceWithDetailsConfiguration { static class PropertySourceWithCustomFactoryConfiguration { } - } + @Nested class ConfigurationClassProxyTests { @@ -384,15 +385,14 @@ void processAheadOfTimeFullConfigurationClass() { getRegisteredBean(CglibConfiguration.class))).isNotNull(); } - private RegisteredBean getRegisteredBean(Class bean) { this.beanFactory.registerBeanDefinition("test", new RootBeanDefinition(bean)); this.processor.postProcessBeanFactory(this.beanFactory); return RegisteredBean.of(this.beanFactory, "test"); } - } + @Nullable private BeanFactoryInitializationAotContribution getContribution(Class... types) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); @@ -410,8 +410,8 @@ private void assertPostProcessorEntry(BeanPostProcessor postProcessor, Class .containsExactly(entry(key.getName(), value.getName())); } - static class CustomPropertySourcesFactory extends DefaultPropertySourceFactory { + static class CustomPropertySourcesFactory extends DefaultPropertySourceFactory { } } diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java index 9bd022ccee57..33c44706d6d1 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -75,7 +75,7 @@ private ReflectUtils() { Throwable throwable = null; try { classLoaderDefineClass = ClassLoader.class.getDeclaredMethod("defineClass", - String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class); + String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class); } catch (Throwable t) { classLoaderDefineClass = null; From 1fb179b057ad1638efb7fde386fe1a168008a754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 22 Apr 2024 14:53:06 +0200 Subject: [PATCH 167/261] Relax scope of DataIntegrityViolationException This commit updates the Javadoc of DataIntegrityViolationException to broaden its scope as it was too specific before. Closes gh-32686 --- .../springframework/dao/DataIntegrityViolationException.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java b/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java index 0e561e04e238..29fa1108803f 100644 --- a/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java +++ b/spring-tx/src/main/java/org/springframework/dao/DataIntegrityViolationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,8 @@ package org.springframework.dao; /** - * Exception thrown when an attempt to insert or update data + * Exception thrown when an attempt to execute an SQL statement fails to map + * the given data, typically but no limited to an insert or update data * results in violation of an integrity constraint. Note that this * is not purely a relational concept; integrity constraints such * as unique primary keys are required by most database types. From d9330bcac7e1c976050dd3fcde766b0b315ce6eb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 23 Apr 2024 16:06:48 +0200 Subject: [PATCH 168/261] Skip close lock if acquired by other thread already Closes gh-32445 (cherry picked from commit d151931f8649adf39e781c6ee4228b31c1fcc13b) --- .../ConcurrentWebSocketSessionDecorator.java | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.java b/spring-websocket/src/main/java/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.java index 158db36802f3..30ddf9cd0869 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -248,30 +248,31 @@ private void limitExceeded(String reason) { @Override public void close(CloseStatus status) throws IOException { - this.closeLock.lock(); - try { - if (this.closeInProgress) { - return; - } - if (!CloseStatus.SESSION_NOT_RELIABLE.equals(status)) { - try { - checkSessionLimits(); - } - catch (SessionLimitExceededException ex) { - // Ignore + if (this.closeLock.tryLock()) { + try { + if (this.closeInProgress) { + return; } - if (this.limitExceeded) { - if (logger.isDebugEnabled()) { - logger.debug("Changing close status " + status + " to SESSION_NOT_RELIABLE."); + if (!CloseStatus.SESSION_NOT_RELIABLE.equals(status)) { + try { + checkSessionLimits(); + } + catch (SessionLimitExceededException ex) { + // Ignore + } + if (this.limitExceeded) { + if (logger.isDebugEnabled()) { + logger.debug("Changing close status " + status + " to SESSION_NOT_RELIABLE."); + } + status = CloseStatus.SESSION_NOT_RELIABLE; } - status = CloseStatus.SESSION_NOT_RELIABLE; } + this.closeInProgress = true; + super.close(status); + } + finally { + this.closeLock.unlock(); } - this.closeInProgress = true; - super.close(status); - } - finally { - this.closeLock.unlock(); } } From 9d2c6f80b88b22714b80911746333f3c76657444 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 23 Apr 2024 16:25:24 +0200 Subject: [PATCH 169/261] Polishing --- .../util/StreamUtilsTests.java | 5 ++- .../ResourceHttpMessageConverter.java | 33 +++++++++-------- .../ResourceRegionHttpMessageConverter.java | 36 +++++++++---------- .../support/InvocableHandlerMethod.java | 3 +- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java index 24330cf6778d..6d3a276c2078 100644 --- a/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -45,6 +45,7 @@ class StreamUtilsTests { private String string = ""; + @BeforeEach void setup() { new Random().nextBytes(bytes); @@ -53,6 +54,7 @@ void setup() { } } + @Test void copyToByteArray() throws Exception { InputStream inputStream = new ByteArrayInputStream(bytes); @@ -127,4 +129,5 @@ void nonClosingOutputStream() throws Exception { ordered.verify(source).write(bytes, 1, 2); ordered.verify(source, never()).close(); } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java index 4f1f34cc8551..49ce0b17114b 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java @@ -82,6 +82,7 @@ protected Resource readInternal(Class clazz, HttpInputMessag if (this.supportsReadStreaming && InputStreamResource.class == clazz) { return new InputStreamResource(inputMessage.getBody()) { @Override + @Nullable public String getFilename() { return inputMessage.getHeaders().getContentDisposition().getFilename(); } @@ -107,6 +108,23 @@ public String getFilename() { } } + @Override + protected void writeInternal(Resource resource, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + writeContent(resource, outputMessage); + } + + /** + * Add the default headers for the given resource to the given message. + * @since 6.0 + */ + public void addDefaultHeaders(HttpOutputMessage message, Resource resource, @Nullable MediaType contentType) + throws IOException { + + addDefaultHeaders(message.getHeaders(), resource, contentType); + } + @Override protected MediaType getDefaultContentType(Resource resource) { return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); @@ -124,23 +142,10 @@ protected Long getContentLength(Resource resource, @Nullable MediaType contentTy return (contentLength < 0 ? null : contentLength); } - /** - * Adds the default headers for the given resource to the given message. - * @since 6.0 - */ - public void addDefaultHeaders(HttpOutputMessage message, Resource resource, @Nullable MediaType contentType) throws IOException { - addDefaultHeaders(message.getHeaders(), resource, contentType); - } - - @Override - protected void writeInternal(Resource resource, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - - writeContent(resource, outputMessage); - } protected void writeContent(Resource resource, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + // We cannot use try-with-resources here for the InputStream, since we have // custom handling of the close() method in a finally-block. try { diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java index 62a2f16347a4..1f143ae2dc6e 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -52,22 +52,6 @@ public ResourceRegionHttpMessageConverter() { } - @Override - @SuppressWarnings("unchecked") - protected MediaType getDefaultContentType(Object object) { - Resource resource = null; - if (object instanceof ResourceRegion resourceRegion) { - resource = resourceRegion.getResource(); - } - else { - Collection regions = (Collection) object; - if (!regions.isEmpty()) { - resource = regions.iterator().next().getResource(); - } - } - return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); - } - @Override public boolean canRead(Class clazz, @Nullable MediaType mediaType) { return false; @@ -119,7 +103,6 @@ public boolean canWrite(@Nullable Type type, @Nullable Class clazz, @Nullable } @Override - @SuppressWarnings("unchecked") protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { @@ -127,6 +110,7 @@ protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessa writeResourceRegion(resourceRegion, outputMessage); } else { + @SuppressWarnings("unchecked") Collection regions = (Collection) object; if (regions.size() == 1) { writeResourceRegion(regions.iterator().next(), outputMessage); @@ -137,6 +121,22 @@ protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessa } } + @Override + protected MediaType getDefaultContentType(Object object) { + Resource resource = null; + if (object instanceof ResourceRegion resourceRegion) { + resource = resourceRegion.getResource(); + } + else { + @SuppressWarnings("unchecked") + Collection regions = (Collection) object; + if (!regions.isEmpty()) { + resource = regions.iterator().next().getResource(); + } + } + return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM); + } + protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException { Assert.notNull(region, "ResourceRegion must not be null"); diff --git a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java index 4fcaec95213b..84e26dd4d8a3 100644 --- a/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java +++ b/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -230,7 +230,6 @@ else if (targetException instanceof Exception exception) { /** * Invoke the given Kotlin coroutine suspended function. - * *

      The default implementation invokes * {@link CoroutinesUtils#invokeSuspendingFunction(Method, Object, Object...)}, * but subclasses can override this method to use From cafb5cfbbe9e79b529dfe825590c17708f0dc764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 24 Apr 2024 11:09:44 +0200 Subject: [PATCH 170/261] Refine preDetermineBeanTypes algorithm This commit refines the preDetermineBeanTypes algorithm that AOT uses to approximate the order of preInstantiateSingletons more closely. Previously, the algorithm assumed that all beans where non-lazy singletons in terms of initialization order, which led to inconsistent order in CGLIB proxy generation. We now explicitly park lazy beans so that their types are determined during a second phase, matching the order of regular initialization order. Closes gh-32701 Co-authored-by: Juergen Hoeller --- .../support/GenericApplicationContext.java | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java index 85da53b39b0f..88728844bf97 100644 --- a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +18,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -423,16 +424,37 @@ private void preDetermineBeanTypes(RuntimeHints runtimeHints) { PostProcessorRegistrationDelegate.loadBeanPostProcessors( this.beanFactory, SmartInstantiationAwareBeanPostProcessor.class); + List lazyBeans = new ArrayList<>(); + + // First round: non-lazy singleton beans in definition order, + // matching preInstantiateSingletons. for (String beanName : this.beanFactory.getBeanDefinitionNames()) { - Class beanType = this.beanFactory.getType(beanName); - if (beanType != null) { - ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints); - for (SmartInstantiationAwareBeanPostProcessor bpp : bpps) { - Class newBeanType = bpp.determineBeanType(beanType, beanName); - if (newBeanType != beanType) { - ClassHintUtils.registerProxyIfNecessary(newBeanType, runtimeHints); - beanType = newBeanType; - } + BeanDefinition bd = getBeanDefinition(beanName); + if (bd.isSingleton() && !bd.isLazyInit()) { + preDetermineBeanType(beanName, bpps, runtimeHints); + } + else { + lazyBeans.add(beanName); + } + } + + // Second round: lazy singleton beans and scoped beans. + for (String beanName : lazyBeans) { + preDetermineBeanType(beanName, bpps, runtimeHints); + } + } + + private void preDetermineBeanType(String beanName, List bpps, + RuntimeHints runtimeHints) { + + Class beanType = this.beanFactory.getType(beanName); + if (beanType != null) { + ClassHintUtils.registerProxyIfNecessary(beanType, runtimeHints); + for (SmartInstantiationAwareBeanPostProcessor bpp : bpps) { + Class newBeanType = bpp.determineBeanType(beanType, beanName); + if (newBeanType != beanType) { + ClassHintUtils.registerProxyIfNecessary(newBeanType, runtimeHints); + beanType = newBeanType; } } } From fb67f97a41b1c0242133ef1029c3105940edcd01 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 29 Apr 2024 13:15:05 +0200 Subject: [PATCH 171/261] Upgrade to gradle-enterprise-conventions 0.0.17 Closes gh-32726 --- .github/workflows/build-and-deploy-snapshot.yml | 4 +--- .github/workflows/ci.yml | 4 +--- ci/pipeline.yml | 4 +--- settings.gradle | 4 ++-- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 3b15f3196b0d..12c119762056 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -35,9 +35,7 @@ jobs: env: CI: 'true' GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository - name: Deploy uses: spring-io/artifactory-deploy-action@v0.0.1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fcff95d00ca4..0622990c7de2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,9 +66,7 @@ jobs: env: CI: 'true' GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} run: ./gradlew check antora - name: Send notification uses: ./.github/actions/send-notification diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 385f1f7c4bd9..770f9ccb1244 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -5,9 +5,7 @@ anchors: password: ((github-ci-release-token)) branch: ((branch)) gradle-enterprise-task-params: &gradle-enterprise-task-params - GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) - GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username)) - GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password)) + DEVELOCITY_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) sonatype-task-params: &sonatype-task-params SONATYPE_USERNAME: ((sonatype-username)) SONATYPE_PASSWORD: ((sonatype-password)) diff --git a/settings.gradle b/settings.gradle index fac8d089dec4..3f1d16385bfb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,8 +7,8 @@ pluginManagement { } plugins { - id "com.gradle.enterprise" version "3.12.6" - id "io.spring.ge.conventions" version "0.0.13" + id "com.gradle.develocity" version "3.17.2" + id "io.spring.ge.conventions" version "0.0.17" } include "spring-aop" From 4e3b834310efbc146efa626167f0ed452bc37fb5 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 30 Apr 2024 11:49:15 +0200 Subject: [PATCH 172/261] Fix guard against multiple subscriptions This commit changes the guard against multiple subscriptions, as the previously used doOnSubscribe hook could not function as guard in certain scenarios. See gh-32727 Closes gh-32732 --- .../reactive/AbstractClientHttpResponse.java | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java index 4b128b047485..60cf8b73f619 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpResponse.java @@ -16,8 +16,12 @@ package org.springframework.http.client.reactive; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; @@ -55,16 +59,7 @@ protected AbstractClientHttpResponse(HttpStatusCode statusCode, HttpHeaders head this.statusCode = statusCode; this.headers = headers; this.cookies = cookies; - this.body = singleSubscription(body); - } - - private static Flux singleSubscription(Flux body) { - AtomicBoolean subscribed = new AtomicBoolean(); - return body.doOnSubscribe(s -> { - if (!subscribed.compareAndSet(false, true)) { - throw new IllegalStateException("The client response body can only be consumed once"); - } - }); + this.body = Flux.from(new SingleSubscriberPublisher<>(body)); } @@ -87,4 +82,39 @@ public MultiValueMap getCookies() { public Flux getBody() { return this.body; } + + + private static final class SingleSubscriberPublisher implements Publisher { + + private static final Subscription NO_OP_SUBSCRIPTION = new Subscription() { + @Override + public void request(long l) { + } + + @Override + public void cancel() { + } + }; + + private final Publisher delegate; + + private final AtomicBoolean subscribed = new AtomicBoolean(); + + + public SingleSubscriberPublisher(Publisher delegate) { + this.delegate = delegate; + } + + @Override + public void subscribe(Subscriber subscriber) { + Objects.requireNonNull(subscriber, "Subscriber must not be null"); + if (this.subscribed.compareAndSet(false, true)) { + this.delegate.subscribe(subscriber); + } + else { + subscriber.onSubscribe(NO_OP_SUBSCRIPTION); + subscriber.onError(new IllegalStateException("The client response body can only be consumed once")); + } + } + } } From 08a63a4180832c653f45cdf512e06e4c661246da Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 30 Apr 2024 19:09:03 +0200 Subject: [PATCH 173/261] Fix build warnings See gh-32726 --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 3f1d16385bfb..adb99dc3e97c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -45,7 +45,7 @@ rootProject.children.each {project -> } settings.gradle.projectsLoaded { - gradleEnterprise { + develocity { buildScan { File buildDir = settings.gradle.rootProject .getLayout().getBuildDirectory().getAsFile().get() From f7b7e29fd7b19383175117c84ffc82c821ba7b58 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 1 May 2024 15:41:26 +0200 Subject: [PATCH 174/261] Ignore non-String keys in PropertiesPropertySource.getPropertyNames() Closes gh-32742 (cherry picked from commit 610626aec69dd8a932dcc2cbee5642d2dcccc3bc) --- .../core/env/PropertiesPropertySource.java | 4 ++-- .../core/env/StandardEnvironmentTests.java | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java index d09c3683510e..9741c5819287 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertiesPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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,7 +48,7 @@ protected PropertiesPropertySource(String name, Map source) { @Override public String[] getPropertyNames() { synchronized (this.source) { - return super.getPropertyNames(); + return ((Map) this.source).keySet().stream().filter(k -> k instanceof String).toArray(String[]::new); } } diff --git a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java index 36c7915ac096..9fe0090437d1 100644 --- a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java @@ -16,7 +16,9 @@ package org.springframework.core.env; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -300,6 +302,12 @@ void getSystemProperties() { assertThat(systemProperties.get(DISALLOWED_PROPERTY_NAME)).isEqualTo(DISALLOWED_PROPERTY_VALUE); assertThat(systemProperties.get(STRING_PROPERTY_NAME)).isEqualTo(NON_STRING_PROPERTY_VALUE); assertThat(systemProperties.get(NON_STRING_PROPERTY_NAME)).isEqualTo(STRING_PROPERTY_VALUE); + + PropertiesPropertySource systemPropertySource = (PropertiesPropertySource) + environment.getPropertySources().get(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME); + Set expectedKeys = new HashSet<>(System.getProperties().stringPropertyNames()); + expectedKeys.add(STRING_PROPERTY_NAME); // filtered out by stringPropertyNames due to non-String value + assertThat(Set.of(systemPropertySource.getPropertyNames())).isEqualTo(expectedKeys); } finally { System.clearProperty(ALLOWED_PROPERTY_NAME); @@ -316,6 +324,7 @@ void getSystemEnvironment() { assertThat(System.getenv()).isSameAs(systemEnvironment); } + @Nested class GetActiveProfiles { @@ -365,6 +374,7 @@ void fromSystemProperties_withMultipleProfiles_withWhitespace() { } } + @Nested class AcceptsProfilesTests { @@ -447,9 +457,9 @@ void withProfileExpression() { environment.addActiveProfile("p2"); assertThat(environment.acceptsProfiles(Profiles.of("p1 & p2"))).isTrue(); } - } + @Nested class MatchesProfilesTests { @@ -559,7 +569,6 @@ void withProfileExpressions() { assertThat(environment.matchesProfiles("p2 & (foo | p1)")).isTrue(); assertThat(environment.matchesProfiles("foo", "(p2 & p1)")).isTrue(); } - } } From 0931769822976a82314b576a25ba7f458a8ef37a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 1 May 2024 15:49:14 +0200 Subject: [PATCH 175/261] Polishing --- .../org/springframework/aop/framework/AdvisedSupport.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java index 477c85c88223..0edad999e43a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -368,8 +368,7 @@ public void addAdvisors(Collection advisors) { private void validateIntroductionAdvisor(IntroductionAdvisor advisor) { advisor.validateInterfaces(); // If the advisor passed validation, we can make the change. - Class[] ifcs = advisor.getInterfaces(); - for (Class ifc : ifcs) { + for (Class ifc : advisor.getInterfaces()) { addInterface(ifc); } } From b11d118376c8a445cb5b42a88c26ae9582997891 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 1 May 2024 15:56:28 +0200 Subject: [PATCH 176/261] Upgrade to Groovy 4.0.21, Netty 4.1.109, Undertow 2.3.13 --- framework-platform/framework-platform.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 953b39ad9f92..9ef10a88b8f0 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,11 +9,11 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.14.3")) api(platform("io.micrometer:micrometer-bom:1.10.13")) - api(platform("io.netty:netty-bom:4.1.108.Final")) + api(platform("io.netty:netty-bom:4.1.109.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2022.0.18")) api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.20")) + api(platform("org.apache.groovy:groovy-bom:4.0.21")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) api(platform("org.eclipse.jetty:jetty-bom:11.0.20")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) @@ -54,9 +54,9 @@ dependencies { api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") api("io.reactivex.rxjava3:rxjava:3.1.8") api("io.smallrye.reactive:mutiny:1.10.0") - api("io.undertow:undertow-core:2.3.12.Final") - api("io.undertow:undertow-servlet:2.3.12.Final") - api("io.undertow:undertow-websockets-jsr:2.3.12.Final") + api("io.undertow:undertow-core:2.3.13.Final") + api("io.undertow:undertow-servlet:2.3.13.Final") + api("io.undertow:undertow-websockets-jsr:2.3.13.Final") api("io.vavr:vavr:0.10.4") api("jakarta.activation:jakarta.activation-api:2.0.1") api("jakarta.annotation:jakarta.annotation-api:2.0.0") From 9a31f3b5a5265344fb7c904d89ba345fbddeb387 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 1 May 2024 18:06:27 +0200 Subject: [PATCH 177/261] Consistently propagate ApplicationStartup to BeanFactory Closes gh-32747 (cherry picked from commit 25cedcfb9959bad9441c45029d62b5df07cc1b11) --- .../beans/factory/support/AbstractBeanFactory.java | 2 +- .../context/support/AbstractRefreshableApplicationContext.java | 3 ++- 2 files changed, 3 insertions(+), 2 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 5488cc0d73b9..1a48ed8d1997 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 @@ -1053,7 +1053,7 @@ public Scope getRegisteredScope(String scopeName) { @Override public void setApplicationStartup(ApplicationStartup applicationStartup) { - Assert.notNull(applicationStartup, "applicationStartup must not be null"); + Assert.notNull(applicationStartup, "ApplicationStartup must not be null"); this.applicationStartup = applicationStartup; } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java index 9c87844e9d71..5b2e5288678e 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -126,6 +126,7 @@ protected final void refreshBeanFactory() throws BeansException { try { DefaultListableBeanFactory beanFactory = createBeanFactory(); beanFactory.setSerializationId(getId()); + beanFactory.setApplicationStartup(getApplicationStartup()); customizeBeanFactory(beanFactory); loadBeanDefinitions(beanFactory); this.beanFactory = beanFactory; From 3b50b6ef943d5fc7d8baf56f3a91308f8fdca424 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 3 May 2024 12:14:44 +0300 Subject: [PATCH 178/261] Include repeatable annotation container in MergedAnnotations results A bug has existed in Spring's MergedAnnotations support since it was introduced in Spring Framework 5.2. Specifically, if the MergedAnnotations API is used to search for annotations with "standard repeatable annotation" support enabled (which is the default), it's possible to search for a repeatable annotation but not for the repeatable annotation's container annotation. The reason is that MergedAnnotationFinder.process(Object, int, Object, Annotation) does not process the container annotation and instead only processes the "contained" annotations, which prevents a container annotation from being included in search results. In #29685, we fixed a bug that prevented the MergedAnnotations support from recognizing an annotation as a container if the container annotation declares attributes other than the required `value` attribute. As a consequence of that bug fix, since Spring Framework 5.3.25, the MergedAnnotations infrastructure considers such an annotation a container, and due to the aforementioned bug the container is no longer processed, which results in a regression in behavior for annotation searches for such a container annotation. This commit addresses the original bug as well as the regression by processing container annotations in addition to the contained repeatable annotations. See gh-29685 Closes gh-32731 (cherry picked from commit 4baad16437524f6f16f61fcfb3dc0458f7aaff47) --- .../annotation/TypeMappedAnnotations.java | 7 +- ...dAnnotationsRepeatableAnnotationTests.java | 64 ++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java index 581a74c10bd6..7187cceb42ae 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -418,7 +418,10 @@ private MergedAnnotation process( Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation); if (repeatedAnnotations != null) { - return doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + MergedAnnotation result = doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations); + if (result != null) { + return result; + } } AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( annotation.annotationType(), repeatableContainers, annotationFilter); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java index 40b1fdf5b8f8..f1a80ee56e73 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java @@ -24,6 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; import java.util.Set; import java.util.stream.Stream; @@ -175,7 +176,7 @@ void typeHierarchyWhenOnClassReturnsAnnotations() { } @Test - void typeHierarchyWhenWhenOnSuperclassReturnsAnnotations() { + void typeHierarchyWhenOnSuperclassReturnsAnnotations() { Set annotations = getAnnotations(null, PeteRepeat.class, SearchStrategy.TYPE_HIERARCHY, SubRepeatableClass.class); assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", @@ -240,6 +241,44 @@ void typeHierarchyAnnotationsWithLocalComposedAnnotationWhoseRepeatableMetaAnnot assertThat(annotationTypes).containsExactly(WithRepeatedMetaAnnotations.class, Noninherited.class, Noninherited.class); } + @Test // gh-32731 + void searchFindsRepeatableContainerAnnotationAndRepeatedAnnotations() { + Class clazz = StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class; + + // NO RepeatableContainers + MergedAnnotations mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.none()); + ContainerWithMultipleAttributes container = mergedAnnotations + .get(ContainerWithMultipleAttributes.class) + .synthesize(MergedAnnotation::isPresent).orElse(null); + assertThat(container).as("container").isNotNull(); + assertThat(container.name()).isEqualTo("enigma"); + RepeatableWithContainerWithMultipleAttributes[] repeatedAnnotations = container.value(); + assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B"); + Set set = + mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + // Only finds the locally declared repeated annotation. + assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("C"); + + // Standard RepeatableContainers + mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.standardRepeatables()); + container = mergedAnnotations + .get(ContainerWithMultipleAttributes.class) + .synthesize(MergedAnnotation::isPresent).orElse(null); + assertThat(container).as("container").isNotNull(); + assertThat(container.name()).isEqualTo("enigma"); + repeatedAnnotations = container.value(); + assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B"); + set = mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class) + .collect(MergedAnnotationCollectors.toAnnotationSet()); + // Finds the locally declared repeated annotation plus the 2 in the container. + assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value)) + .containsExactly("A", "B", "C"); + } + private Set getAnnotations(Class container, Class repeatable, SearchStrategy searchStrategy, AnnotatedElement element) { @@ -449,4 +488,27 @@ static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass static class WithRepeatedMetaAnnotationsClass { } + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithMultipleAttributes { + + RepeatableWithContainerWithMultipleAttributes[] value(); + + String name() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(ContainerWithMultipleAttributes.class) + @interface RepeatableWithContainerWithMultipleAttributes { + + String value() default ""; + } + + @ContainerWithMultipleAttributes(name = "enigma", value = { + @RepeatableWithContainerWithMultipleAttributes("A"), + @RepeatableWithContainerWithMultipleAttributes("B") + }) + @RepeatableWithContainerWithMultipleAttributes("C") + static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase { + } + } From 9c775d26436717e17bcae8b22cdd12121a75b203 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 3 May 2024 12:37:42 +0300 Subject: [PATCH 179/261] Fix compilation error in test --- .../MergedAnnotationsRepeatableAnnotationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java index f1a80ee56e73..d3391ec35eb4 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java @@ -246,7 +246,7 @@ void searchFindsRepeatableContainerAnnotationAndRepeatedAnnotations() { Class clazz = StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class; // NO RepeatableContainers - MergedAnnotations mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.none()); + MergedAnnotations mergedAnnotations = MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none()); ContainerWithMultipleAttributes container = mergedAnnotations .get(ContainerWithMultipleAttributes.class) .synthesize(MergedAnnotation::isPresent).orElse(null); @@ -263,7 +263,7 @@ void searchFindsRepeatableContainerAnnotationAndRepeatedAnnotations() { .containsExactly("C"); // Standard RepeatableContainers - mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.standardRepeatables()); + mergedAnnotations = MergedAnnotations.from(clazz, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.standardRepeatables()); container = mergedAnnotations .get(ContainerWithMultipleAttributes.class) .synthesize(MergedAnnotation::isPresent).orElse(null); From dfe437ac887b5058fb11edcfed88cc233eeba1f1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 6 May 2024 20:09:43 +0200 Subject: [PATCH 180/261] Unwrap raw target Query instance in case of proxy mismatch Closes gh-32766 (cherry picked from commit 59a125d06f89ec4f0efe0628fe3a70ab143a5360) --- .../orm/jpa/SharedEntityManagerCreator.java | 6 +- .../jpa/SharedEntityManagerCreatorTests.java | 120 ++++++++++-------- 2 files changed, 74 insertions(+), 52 deletions(-) diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index 4e32abcd399a..1d976c3b7333 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -385,7 +385,9 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl else if (targetClass.isInstance(proxy)) { return proxy; } - break; + else { + return this.target.unwrap(targetClass); + } case "getOutputParameterValue": if (this.entityManager == null) { Object key = args[0]; diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java index f30bbdfde8aa..7bc31f679dac 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/SharedEntityManagerCreatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,16 +37,16 @@ import static org.mockito.Mockito.withSettings; /** - * Unit tests for {@link SharedEntityManagerCreator}. + * Tests for {@link SharedEntityManagerCreator}. * * @author Oliver Gierke * @author Juergen Hoeller */ @ExtendWith(MockitoExtension.class) -public class SharedEntityManagerCreatorTests { +class SharedEntityManagerCreatorTests { @Test - public void proxyingWorksIfInfoReturnsNullEntityManagerInterface() { + void proxyingWorksIfInfoReturnsNullEntityManagerInterface() { EntityManagerFactory emf = mock(EntityManagerFactory.class, withSettings().extraInterfaces(EntityManagerFactoryInfo.class)); // EntityManagerFactoryInfo.getEntityManagerInterface returns null @@ -54,7 +54,7 @@ public void proxyingWorksIfInfoReturnsNullEntityManagerInterface() { } @Test - public void transactionRequiredExceptionOnJoinTransaction() { + void transactionRequiredExceptionOnJoinTransaction() { EntityManagerFactory emf = mock(); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy( @@ -62,7 +62,7 @@ public void transactionRequiredExceptionOnJoinTransaction() { } @Test - public void transactionRequiredExceptionOnFlush() { + void transactionRequiredExceptionOnFlush() { EntityManagerFactory emf = mock(); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy( @@ -70,7 +70,7 @@ public void transactionRequiredExceptionOnFlush() { } @Test - public void transactionRequiredExceptionOnPersist() { + void transactionRequiredExceptionOnPersist() { EntityManagerFactory emf = mock(); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy(() -> @@ -78,7 +78,7 @@ public void transactionRequiredExceptionOnPersist() { } @Test - public void transactionRequiredExceptionOnMerge() { + void transactionRequiredExceptionOnMerge() { EntityManagerFactory emf = mock(); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy(() -> @@ -86,7 +86,7 @@ public void transactionRequiredExceptionOnMerge() { } @Test - public void transactionRequiredExceptionOnRemove() { + void transactionRequiredExceptionOnRemove() { EntityManagerFactory emf = mock(); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy(() -> @@ -94,7 +94,7 @@ public void transactionRequiredExceptionOnRemove() { } @Test - public void transactionRequiredExceptionOnRefresh() { + void transactionRequiredExceptionOnRefresh() { EntityManagerFactory emf = mock(); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); assertThatExceptionOfType(TransactionRequiredException.class).isThrownBy(() -> @@ -102,78 +102,98 @@ public void transactionRequiredExceptionOnRefresh() { } @Test - public void deferredQueryWithUpdate() { + void deferredQueryWithUpdate() { EntityManagerFactory emf = mock(); EntityManager targetEm = mock(); - Query query = mock(); + Query targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createQuery("x")).willReturn(query); + given(targetEm.createQuery("x")).willReturn(targetQuery); given(targetEm.isOpen()).willReturn(true); + given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); - em.createQuery("x").executeUpdate(); + Query query = em.createQuery("x"); + assertThat((Query) query.unwrap(null)).isSameAs(targetQuery); + assertThat((Query) query.unwrap(targetQuery.getClass())).isSameAs(targetQuery); + assertThat(query.unwrap(Query.class)).isSameAs(query); + query.executeUpdate(); - verify(query).executeUpdate(); + verify(targetQuery).executeUpdate(); verify(targetEm).close(); } @Test - public void deferredQueryWithSingleResult() { + void deferredQueryWithSingleResult() { EntityManagerFactory emf = mock(); EntityManager targetEm = mock(); - Query query = mock(); + Query targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createQuery("x")).willReturn(query); + given(targetEm.createQuery("x")).willReturn(targetQuery); given(targetEm.isOpen()).willReturn(true); + given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); - em.createQuery("x").getSingleResult(); + Query query = em.createQuery("x"); + assertThat((Query) query.unwrap(null)).isSameAs(targetQuery); + assertThat((Query) query.unwrap(targetQuery.getClass())).isSameAs(targetQuery); + assertThat(query.unwrap(Query.class)).isSameAs(query); + query.getSingleResult(); - verify(query).getSingleResult(); + verify(targetQuery).getSingleResult(); verify(targetEm).close(); } @Test - public void deferredQueryWithResultList() { + void deferredQueryWithResultList() { EntityManagerFactory emf = mock(); EntityManager targetEm = mock(); - Query query = mock(); + Query targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createQuery("x")).willReturn(query); + given(targetEm.createQuery("x")).willReturn(targetQuery); given(targetEm.isOpen()).willReturn(true); + given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); - em.createQuery("x").getResultList(); + Query query = em.createQuery("x"); + assertThat((Query) query.unwrap(null)).isSameAs(targetQuery); + assertThat((Query) query.unwrap(targetQuery.getClass())).isSameAs(targetQuery); + assertThat(query.unwrap(Query.class)).isSameAs(query); + query.getResultList(); - verify(query).getResultList(); + verify(targetQuery).getResultList(); verify(targetEm).close(); } @Test - public void deferredQueryWithResultStream() { + void deferredQueryWithResultStream() { EntityManagerFactory emf = mock(); EntityManager targetEm = mock(); - Query query = mock(); + Query targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createQuery("x")).willReturn(query); + given(targetEm.createQuery("x")).willReturn(targetQuery); given(targetEm.isOpen()).willReturn(true); + given((Query) targetQuery.unwrap(targetQuery.getClass())).willReturn(targetQuery); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); - em.createQuery("x").getResultStream(); + Query query = em.createQuery("x"); + assertThat((Query) query.unwrap(null)).isSameAs(targetQuery); + assertThat((Query) query.unwrap(targetQuery.getClass())).isSameAs(targetQuery); + assertThat(query.unwrap(Query.class)).isSameAs(query); + query.getResultStream(); - verify(query).getResultStream(); + verify(targetQuery).getResultStream(); verify(targetEm).close(); } @Test - public void deferredStoredProcedureQueryWithIndexedParameters() { + void deferredStoredProcedureQueryWithIndexedParameters() { EntityManagerFactory emf = mock(); EntityManager targetEm = mock(); - StoredProcedureQuery query = mock(); + StoredProcedureQuery targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createStoredProcedureQuery("x")).willReturn(query); - willReturn("y").given(query).getOutputParameterValue(0); - willReturn("z").given(query).getOutputParameterValue(2); + given(targetEm.createStoredProcedureQuery("x")).willReturn(targetQuery); + willReturn("y").given(targetQuery).getOutputParameterValue(0); + willReturn("z").given(targetQuery).getOutputParameterValue(2); given(targetEm.isOpen()).willReturn(true); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); @@ -187,24 +207,24 @@ public void deferredStoredProcedureQueryWithIndexedParameters() { spq.getOutputParameterValue(1)); assertThat(spq.getOutputParameterValue(2)).isEqualTo("z"); - verify(query).registerStoredProcedureParameter(0, String.class, ParameterMode.OUT); - verify(query).registerStoredProcedureParameter(1, Number.class, ParameterMode.IN); - verify(query).registerStoredProcedureParameter(2, Object.class, ParameterMode.INOUT); - verify(query).execute(); + verify(targetQuery).registerStoredProcedureParameter(0, String.class, ParameterMode.OUT); + verify(targetQuery).registerStoredProcedureParameter(1, Number.class, ParameterMode.IN); + verify(targetQuery).registerStoredProcedureParameter(2, Object.class, ParameterMode.INOUT); + verify(targetQuery).execute(); verify(targetEm).close(); - verifyNoMoreInteractions(query); + verifyNoMoreInteractions(targetQuery); verifyNoMoreInteractions(targetEm); } @Test - public void deferredStoredProcedureQueryWithNamedParameters() { + void deferredStoredProcedureQueryWithNamedParameters() { EntityManagerFactory emf = mock(); EntityManager targetEm = mock(); - StoredProcedureQuery query = mock(); + StoredProcedureQuery targetQuery = mock(); given(emf.createEntityManager()).willReturn(targetEm); - given(targetEm.createStoredProcedureQuery("x")).willReturn(query); - willReturn("y").given(query).getOutputParameterValue("a"); - willReturn("z").given(query).getOutputParameterValue("c"); + given(targetEm.createStoredProcedureQuery("x")).willReturn(targetQuery); + willReturn("y").given(targetQuery).getOutputParameterValue("a"); + willReturn("z").given(targetQuery).getOutputParameterValue("c"); given(targetEm.isOpen()).willReturn(true); EntityManager em = SharedEntityManagerCreator.createSharedEntityManager(emf); @@ -218,12 +238,12 @@ public void deferredStoredProcedureQueryWithNamedParameters() { spq.getOutputParameterValue("b")); assertThat(spq.getOutputParameterValue("c")).isEqualTo("z"); - verify(query).registerStoredProcedureParameter("a", String.class, ParameterMode.OUT); - verify(query).registerStoredProcedureParameter("b", Number.class, ParameterMode.IN); - verify(query).registerStoredProcedureParameter("c", Object.class, ParameterMode.INOUT); - verify(query).execute(); + verify(targetQuery).registerStoredProcedureParameter("a", String.class, ParameterMode.OUT); + verify(targetQuery).registerStoredProcedureParameter("b", Number.class, ParameterMode.IN); + verify(targetQuery).registerStoredProcedureParameter("c", Object.class, ParameterMode.INOUT); + verify(targetQuery).execute(); verify(targetEm).close(); - verifyNoMoreInteractions(query); + verifyNoMoreInteractions(targetQuery); verifyNoMoreInteractions(targetEm); } From 43dd22ba31bb9325f49337786af97b7cf69c90c3 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 6 May 2024 20:10:40 +0200 Subject: [PATCH 181/261] Polishing (cherry picked from commit 05d9b52b19e1f740001abe40640fc02fd3001355) --- .../quartz/SchedulerFactoryBeanRuntimeHints.java | 6 +++--- ...ocessorBeanFactoryInitializationAotProcessor.java | 12 +++++++----- .../aot/hint/annotation/Reflective.java | 5 ++--- .../annotation/RegisterReflectionForBinding.java | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java index c2d727a45588..67e69ed07fd4 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,7 +36,7 @@ class SchedulerFactoryBeanRuntimeHints implements RuntimeHintsRegistrar { private static final String SCHEDULER_FACTORY_CLASS_NAME = "org.quartz.impl.StdSchedulerFactory"; - private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar(); + private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); @Override @@ -48,7 +48,7 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { .registerType(TypeReference.of(SCHEDULER_FACTORY_CLASS_NAME), this::typeHint) .registerTypes(TypeReference.listOf(ResourceLoaderClassLoadHelper.class, LocalTaskExecutorThreadPool.class, LocalDataSourceJobStore.class), this::typeHint); - this.reflectiveRegistrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class); + registrar.registerRuntimeHints(hints, LocalTaskExecutorThreadPool.class); } private void typeHint(Builder typeHint) { diff --git a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java index 238350ffc226..9cba020aef54 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -39,7 +39,8 @@ */ class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { - private static final ReflectiveRuntimeHintsRegistrar REGISTRAR = new ReflectiveRuntimeHintsRegistrar(); + private static final ReflectiveRuntimeHintsRegistrar registrar = new ReflectiveRuntimeHintsRegistrar(); + @Override public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { @@ -49,7 +50,9 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableL return new ReflectiveProcessorBeanFactoryInitializationAotContribution(beanTypes); } - private static class ReflectiveProcessorBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution { + + private static class ReflectiveProcessorBeanFactoryInitializationAotContribution + implements BeanFactoryInitializationAotContribution { private final Class[] types; @@ -60,9 +63,8 @@ public ReflectiveProcessorBeanFactoryInitializationAotContribution(Class[] ty @Override public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { RuntimeHints runtimeHints = generationContext.getRuntimeHints(); - REGISTRAR.registerRuntimeHints(runtimeHints, this.types); + registrar.registerRuntimeHints(runtimeHints, this.types); } - } } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java index e12ceae9d490..02dcdcef6ba4 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/Reflective.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -39,8 +39,7 @@ * @see ReflectiveRuntimeHintsRegistrar * @see RegisterReflectionForBinding @RegisterReflectionForBinding */ -@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.CONSTRUCTOR, - ElementType.FIELD, ElementType.METHOD }) +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Reflective { diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java index d0a5b9c98ba9..d8e8be8adadb 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java @@ -36,7 +36,7 @@ * *

        * @Configuration
      - * @RegisterReflectionForBinding({ Foo.class, Bar.class })
      + * @RegisterReflectionForBinding({Foo.class, Bar.class})
        * public class MyConfig {
        *     // ...
        * }
      @@ -78,7 +78,7 @@ /** * Classes for which reflection hints should be registered. *

      At least one class must be specified either via {@link #value} or - * {@link #classes}. + * {@code #classes}. * @see #value() */ @AliasFor("value") From 77951dc622c9c9c0770cb8669cd10bb47493fccc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 7 May 2024 15:43:17 +0200 Subject: [PATCH 182/261] Consistent RuntimeHintsRegistrar signature (plus related polishing) --- .../quartz/SchedulerFactoryBeanRuntimeHints.java | 5 +++-- .../annotation/RegisterReflectionForBinding.java | 7 +++---- .../support/SpringFactoriesLoaderRuntimeHints.java | 11 +++++++---- .../EmbeddedDatabaseFactoryRuntimeHints.java | 11 ++++++----- .../orm/jpa/EntityManagerRuntimeHints.java | 7 +++++-- .../annotation/TransactionRuntimeHints.java | 13 +++++++------ .../converter/json/JacksonModulesRuntimeHints.java | 7 ++++--- .../converter/json/ProblemDetailRuntimeHints.java | 7 ++++--- .../web/util/WebUtilRuntimeHints.java | 9 +++++---- .../HandshakeWebSocketServiceRuntimeHints.java | 10 ++++++---- .../support/HandshakeHandlerRuntimeHints.java | 9 ++++++--- 11 files changed, 56 insertions(+), 40 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java index 67e69ed07fd4..ee5e43c05c39 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java @@ -22,11 +22,12 @@ import org.springframework.aot.hint.TypeHint.Builder; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** * {@link RuntimeHintsRegistrar} implementation that makes sure {@link SchedulerFactoryBean} - * reflection entries are registered. + * reflection hints are registered. * * @author Sebastien Deleuze * @author Stephane Nicoll @@ -40,7 +41,7 @@ class SchedulerFactoryBeanRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { if (!ClassUtils.isPresent(SCHEDULER_FACTORY_CLASS_NAME, classLoader)) { return; } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java index d8e8be8adadb..6062af631e04 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,7 @@ * @see org.springframework.aot.hint.BindingReflectionHintsRegistrar * @see Reflective @Reflective */ -@Target({ ElementType.TYPE, ElementType.METHOD }) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Reflective(RegisterReflectionForBindingProcessor.class) @@ -77,8 +77,7 @@ /** * Classes for which reflection hints should be registered. - *

      At least one class must be specified either via {@link #value} or - * {@code #classes}. + *

      At least one class must be specified either via {@link #value} or {@code classes}. * @see #value() */ @AliasFor("value") diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java index 9d8318e438da..8081223c5215 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -47,9 +47,11 @@ class SpringFactoriesLoaderRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + ClassLoader classLoaderToUse = (classLoader != null ? classLoader : + SpringFactoriesLoaderRuntimeHints.class.getClassLoader()); for (String resourceLocation : RESOURCE_LOCATIONS) { - registerHints(hints, classLoader, resourceLocation); + registerHints(hints, classLoaderToUse, resourceLocation); } } @@ -63,6 +65,7 @@ private void registerHints(RuntimeHints hints, ClassLoader classLoader, String r private void registerHints(RuntimeHints hints, ClassLoader classLoader, String factoryClassName, List implementationClassNames) { + Class factoryClass = resolveClassName(classLoader, factoryClassName); if (factoryClass == null) { if (logger.isTraceEnabled()) { @@ -100,6 +103,7 @@ private Class resolveClassName(ClassLoader classLoader, String factoryClassNa } } + private static class ExtendedSpringFactoriesLoader extends SpringFactoriesLoader { ExtendedSpringFactoriesLoader(@Nullable ClassLoader classLoader, Map> factories) { @@ -109,7 +113,6 @@ private static class ExtendedSpringFactoriesLoader extends SpringFactoriesLoader static Map> accessLoadFactoriesResource(ClassLoader classLoader, String resourceLocation) { return SpringFactoriesLoader.loadFactoriesResource(classLoader, resourceLocation); } - } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHints.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHints.java index f39c3cc0e2b5..9f44861ed8d0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHints.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,11 +21,12 @@ import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.lang.Nullable; /** - * {@link RuntimeHintsRegistrar} implementation that registers reflection hints for - * {@code EmbeddedDataSourceProxy#shutdown} in order to allow it to be used as a bean - * destroy method. + * {@link RuntimeHintsRegistrar} implementation that registers reflection hints + * for {@code EmbeddedDataSourceProxy#shutdown} in order to allow it to be used + * as a bean destroy method. * * @author Sebastien Deleuze * @since 6.0 @@ -33,7 +34,7 @@ class EmbeddedDatabaseFactoryRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.reflection().registerTypeIfPresent(classLoader, "org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory$EmbeddedDataSourceProxy", builder -> builder diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java index eb6e204028d2..72891dffdc02 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -22,6 +22,7 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -41,8 +42,9 @@ class EntityManagerRuntimeHints implements RuntimeHintsRegistrar { private static final String NATIVE_QUERY_IMPL_CLASS_NAME = "org.hibernate.query.sql.internal.NativeQueryImpl"; + @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { if (ClassUtils.isPresent(HIBERNATE_SESSION_FACTORY_CLASS_NAME, classLoader)) { hints.proxies().registerJdkProxy(TypeReference.of(HIBERNATE_SESSION_FACTORY_CLASS_NAME), TypeReference.of(EntityManagerFactoryInfo.class)); @@ -70,4 +72,5 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { catch (ClassNotFoundException ignored) { } } + } diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionRuntimeHints.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionRuntimeHints.java index f5cdc81adf75..6b2ce490f65f 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionRuntimeHints.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,11 +21,12 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; +import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionDefinition; /** - * {@link RuntimeHintsRegistrar} implementation that registers runtime hints for - * transaction management. + * {@link RuntimeHintsRegistrar} implementation that registers runtime hints + * for transaction management. * * @author Sebastien Deleuze * @since 6.0 @@ -34,9 +35,9 @@ class TransactionRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { - hints.reflection().registerTypes(TypeReference.listOf( - Isolation.class, Propagation.class, TransactionDefinition.class), + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + hints.reflection().registerTypes( + TypeReference.listOf(Isolation.class, Propagation.class, TransactionDefinition.class), TypeHint.builtWith(MemberCategory.DECLARED_FIELDS)); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/JacksonModulesRuntimeHints.java b/spring-web/src/main/java/org/springframework/http/converter/json/JacksonModulesRuntimeHints.java index b6619f1f20d7..f7f07a9a7f63 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/JacksonModulesRuntimeHints.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/JacksonModulesRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -22,9 +22,10 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeHint.Builder; +import org.springframework.lang.Nullable; /** - * {@link RuntimeHintsRegistrar} implementation that registers reflection entries + * {@link RuntimeHintsRegistrar} implementation that registers reflection hints * for {@link Jackson2ObjectMapperBuilder} well-known modules. * * @author Sebastien Deleuze @@ -38,7 +39,7 @@ class JacksonModulesRuntimeHints implements RuntimeHintsRegistrar { .withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.reflection() .registerTypeIfPresent(classLoader, "com.fasterxml.jackson.datatype.jdk8.Jdk8Module", asJacksonModule) diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/ProblemDetailRuntimeHints.java b/spring-web/src/main/java/org/springframework/http/converter/json/ProblemDetailRuntimeHints.java index 3f569c44d3e7..d04fda433f05 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/ProblemDetailRuntimeHints.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/ProblemDetailRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,10 +20,11 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.http.ProblemDetail; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** - * {@link RuntimeHintsRegistrar} implementation that registers binding reflection entries + * {@link RuntimeHintsRegistrar} implementation that registers binding reflection hints * for {@link ProblemDetail} serialization support with Jackson. * * @author Brian Clozel @@ -33,7 +34,7 @@ class ProblemDetailRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); bindingRegistrar.registerReflectionHints(hints.reflection(), ProblemDetail.class); if (ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader)) { diff --git a/spring-web/src/main/java/org/springframework/web/util/WebUtilRuntimeHints.java b/spring-web/src/main/java/org/springframework/web/util/WebUtilRuntimeHints.java index 06976e58cdbc..dfff61eea9cb 100644 --- a/spring-web/src/main/java/org/springframework/web/util/WebUtilRuntimeHints.java +++ b/spring-web/src/main/java/org/springframework/web/util/WebUtilRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,10 +19,11 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.core.io.ClassPathResource; +import org.springframework.lang.Nullable; /** - * {@link RuntimeHintsRegistrar} implementation that registers resource - * hints for web util resources. + * {@link RuntimeHintsRegistrar} implementation that registers resource hints + * for resources in the {@code web.util} package. * * @author Sebastien Deleuze * @since 6.0 @@ -30,7 +31,7 @@ class WebUtilRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.resources().registerResource( new ClassPathResource("HtmlCharacterEntityReferences.properties", getClass())); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketServiceRuntimeHints.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketServiceRuntimeHints.java index f10e55ce5e49..d931aa3ea268 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketServiceRuntimeHints.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/server/support/HandshakeWebSocketServiceRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,10 +19,11 @@ import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.lang.Nullable; /** - * {@link RuntimeHintsRegistrar} implementation that registers reflection hints related to - * {@link HandshakeWebSocketService}. + * {@link RuntimeHintsRegistrar} implementation that registers reflection hints + * related to {@link HandshakeWebSocketService}. * * @author Sebastien Deleuze * @since 6.0 @@ -30,8 +31,9 @@ class HandshakeWebSocketServiceRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.reflection().registerType(HandshakeWebSocketService.initUpgradeStrategy().getClass(), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); } + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/HandshakeHandlerRuntimeHints.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/HandshakeHandlerRuntimeHints.java index fb00ca8f8380..0e8fbe82e269 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/HandshakeHandlerRuntimeHints.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/HandshakeHandlerRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,10 +21,11 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** - * {@link RuntimeHintsRegistrar} implementation that registers reflection entries + * {@link RuntimeHintsRegistrar} implementation that registers reflection hints * for {@link AbstractHandshakeHandler}. * * @author Sebastien Deleuze @@ -60,8 +61,9 @@ class HandshakeHandlerRuntimeHints implements RuntimeHintsRegistrar { "com.ibm.websphere.wsoc.WsWsocServerContainer", classLoader); } + @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { ReflectionHints reflectionHints = hints.reflection(); if (tomcatWsPresent) { registerType(reflectionHints, "org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy"); @@ -87,4 +89,5 @@ private void registerType(ReflectionHints reflectionHints, String className) { reflectionHints.registerType(TypeReference.of(className), builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); } + } From 427a96befc5ca5c4ab00cbef99ad334ca646d103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 8 May 2024 15:21:47 +0200 Subject: [PATCH 183/261] Adapt docs deployment properties This commit fixes the artifact properties we set for "framework-docs" artifacts. These have a different name as of 6.1.x and were backported as is. Closes gh-32780 --- .github/workflows/build-and-deploy-snapshot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 12c119762056..b3f12ea4f8db 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -49,9 +49,9 @@ jobs: signing-key: ${{ secrets.GPG_PRIVATE_KEY }} signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} artifact-properties: | - /**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false - /**/framework-api-*-docs.zip::zip.type=docs - /**/framework-api-*-schema.zip::zip.type=schema + /**/framework-docs-*.zip::zip.name=spring-framework,zip.deployed=false + /**/framework-docs-*-docs.zip::zip.type=docs + /**/framework-docs-*-schema.zip::zip.type=schema - name: Send notification uses: ./.github/actions/send-notification if: always() From c1f3e37acd6f853b92cc035254bbf705f3724b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 8 May 2024 15:35:41 +0200 Subject: [PATCH 184/261] Polish --- .github/workflows/build-and-deploy-snapshot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index b3f12ea4f8db..0b9103ff357c 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -51,6 +51,7 @@ jobs: artifact-properties: | /**/framework-docs-*.zip::zip.name=spring-framework,zip.deployed=false /**/framework-docs-*-docs.zip::zip.type=docs + /**/framework-docs-*-dist.zip::zip.type=dist /**/framework-docs-*-schema.zip::zip.type=schema - name: Send notification uses: ./.github/actions/send-notification From f3f3063091c99cb9e04a1a01c8ae3b4ea97be865 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 12 May 2024 12:29:52 +0200 Subject: [PATCH 185/261] Fix Dokka links to Spring Framework and Servlet APIs This commit fixes links from Spring Framework's Dokka HTML to Javadoc for Spring Framework and Servlet APIs by explicitly configuring the `element-list` page as the `package-list` in the Dokka Gradle plugin. Closes gh-32797 (cherry picked from commit 7536980be9525d39fce7230b730efe1500174c44) --- gradle/docs-dokka.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle/docs-dokka.gradle b/gradle/docs-dokka.gradle index 147c39497f2a..7d593bf49de1 100644 --- a/gradle/docs-dokka.gradle +++ b/gradle/docs-dokka.gradle @@ -6,6 +6,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure { classpath.from(sourceSets["main"].runtimeClasspath) externalDocumentationLink { url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/")) + packageListUrl.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/element-list")) } externalDocumentationLink { url.set(new URL("https://projectreactor.io/docs/core/release/api/")) @@ -21,6 +22,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure { } externalDocumentationLink { url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/")) + packageListUrl.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list")) } externalDocumentationLink { url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/")) From 8b6a54c45fa60ead74072a5da25102b3b3cdadd0 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 13 May 2024 10:49:02 +0100 Subject: [PATCH 186/261] Update MockMvc section on Streaming in the docs Closes gh-32687 --- .../vs-end-to-end-integration-tests.adoc | 2 +- .../vs-streaming-response.adoc | 45 +++++-------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc index 9b26c80f2370..39f6af5aecbc 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-end-to-end-integration-tests.adoc @@ -1,7 +1,7 @@ [[spring-mvc-test-vs-end-to-end-integration-tests]] = MockMvc vs End-to-End Tests -MockMVc is built on Servlet API mock implementations from the +MockMvc is built on Servlet API mock implementations from the `spring-test` module and does not rely on a running container. Therefore, there are some differences when compared to full end-to-end integration tests with an actual client and a live server running. diff --git a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc index c5c3e7dc5a2c..e6719139038d 100644 --- a/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc +++ b/framework-docs/modules/ROOT/pages/testing/spring-mvc-test-framework/vs-streaming-response.adoc @@ -1,38 +1,15 @@ [[spring-mvc-test-vs-streaming-response]] = Streaming Responses -The best way to test streaming responses such as Server-Sent Events is through the -<> which can be used as a test client to connect to a `MockMvc` instance -to perform tests on Spring MVC controllers without a running server. For example: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes",role="primary"] ----- - WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build(); - - FluxExchangeResult exchangeResult = client.get() - .uri("/persons") - .exchange() - .expectStatus().isOk() - .expectHeader().contentType("text/event-stream") - .returnResult(Person.class); - - // Use StepVerifier from Project Reactor to test the streaming response - - StepVerifier.create(exchangeResult.getResponseBody()) - .expectNext(new Person("N0"), new Person("N1"), new Person("N2")) - .expectNextCount(4) - .consumeNextWith(person -> assertThat(person.getName()).endsWith("7")) - .thenCancel() - .verify(); ----- -====== - -`WebTestClient` can also connect to a live server and perform full end-to-end integration -tests. This is also supported in Spring Boot where you can -{docs-spring-boot}/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test a running server]. - +You can use `WebTestClient` to test xref:testing/webtestclient.adoc#webtestclient-stream[streaming responses] +such as Server-Sent Events. However, `MockMvcWebTestClient` doesn't support infinite +streams because there is no way to cancel the server stream from the client side. +To test infinite streams, you'll need to +xref:testing/webtestclient.adoc#webtestclient-server-config[bind to] a running server, +or when using Spring Boot, +{docs-spring-boot}/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test with a running server]. + +`MockMvcWebTestClient` does support asynchronous responses, and even streaming responses. +The limitation is that it can't influence the server to stop, and therefore the server +must finish writing the response on its own. From 5288504ceb17652945e6a77ed5606474466807a0 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 13 May 2024 11:14:02 +0100 Subject: [PATCH 187/261] Use instance field for ProblemDetail in ErrorResponse's Closes gh-32644 --- .../main/java/org/springframework/web/ErrorResponse.java | 5 +++++ .../request/async/AsyncRequestTimeoutException.java | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java index 2ae0187d705c..3bc3cb26792d 100644 --- a/spring-web/src/main/java/org/springframework/web/ErrorResponse.java +++ b/spring-web/src/main/java/org/springframework/web/ErrorResponse.java @@ -61,6 +61,11 @@ default HttpHeaders getHeaders() { * Return the body for the response, formatted as an RFC 7807 * {@link ProblemDetail} whose {@link ProblemDetail#getStatus() status} * should match the response status. + *

      Note: The returned {@code ProblemDetail} may be + * updated before the response is rendered, e.g. via + * {@link #updateAndGetBody(MessageSource, Locale)}. Therefore, implementing + * methods should use an instance field, and should not re-create the + * {@code ProblemDetail} on every call, nor use a static variable. */ ProblemDetail getBody(); diff --git a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestTimeoutException.java b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestTimeoutException.java index 100dd593032b..ea0e57f6085a 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestTimeoutException.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/async/AsyncRequestTimeoutException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,6 +37,9 @@ @SuppressWarnings("serial") public class AsyncRequestTimeoutException extends RuntimeException implements ErrorResponse { + private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode()); + + @Override public HttpStatusCode getStatusCode() { return HttpStatus.SERVICE_UNAVAILABLE; @@ -44,7 +47,7 @@ public HttpStatusCode getStatusCode() { @Override public ProblemDetail getBody() { - return ProblemDetail.forStatus(getStatusCode()); + return this.body; } } From 09f23a578bd50dee8a67031db34bc546ecce1d62 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 13 May 2024 11:38:58 +0100 Subject: [PATCH 188/261] Update docs on HandlerInterceptor Closes gh-32729 --- .../web/webmvc/mvc-config/interceptors.adoc | 12 ++--- .../handlermapping-interceptor.adoc | 45 +++++++++---------- .../web/servlet/HandlerInterceptor.java | 14 ++++-- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc index 36c7d32956e5..aa9f1f8a4277 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/interceptors.adoc @@ -52,14 +52,10 @@ The following example shows how to achieve the same configuration in XML: ---- -NOTE: Interceptors are not ideally suited as a security layer due to the potential -for a mismatch with annotated controller path matching, which can also match trailing -slashes and path extensions transparently, along with other path matching options. Many -of these options have been deprecated but the potential for a mismatch remains. -Generally, we recommend using Spring Security which includes a dedicated -https://docs.spring.io/spring-security/reference/servlet/integrations/mvc.html#mvc-requestmatcher[MvcRequestMatcher] -to align with Spring MVC path matching and also has a security firewall that blocks many -unwanted characters in URL paths. +WARNING: Interceptors are not ideally suited as a security layer due to the potential for +a mismatch with annotated controller path matching. Generally, we recommend using Spring +Security, or alternatively a similar approach integrated with the Servlet filter chain, +and applied as early as possible. NOTE: The XML config declares interceptors as `MappedInterceptor` beans, and those are in turn detected by any `HandlerMapping` bean, including those from other frameworks. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc index 95aa38fbd334..f153256002e5 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/handlermapping-interceptor.adoc @@ -1,34 +1,29 @@ [[mvc-handlermapping-interceptor]] = Interception -All `HandlerMapping` implementations support handler interceptors that are useful when -you want to apply specific functionality to certain requests -- for example, checking for -a principal. Interceptors must implement `HandlerInterceptor` from the -`org.springframework.web.servlet` package with three methods that should provide enough -flexibility to do all kinds of pre-processing and post-processing: - -* `preHandle(..)`: Before the actual handler is run -* `postHandle(..)`: After the handler is run -* `afterCompletion(..)`: After the complete request has finished - -The `preHandle(..)` method returns a boolean value. You can use this method to break or -continue the processing of the execution chain. When this method returns `true`, the -handler execution chain continues. When it returns false, the `DispatcherServlet` -assumes the interceptor itself has taken care of requests (and, for example, rendered an -appropriate view) and does not continue executing the other interceptors and the actual -handler in the execution chain. +All `HandlerMapping` implementations support handler interception which is useful when +you want to apply functionality across requests. A `HandlerInterceptor` can implement the +following: + +* `preHandle(..)` -- callback before the actual handler is run that returns a boolean. +If the method returns `true`, execution continues; if it returns `false`, the rest of the +execution chain is bypassed and the handler is not called. +* `postHandle(..)` -- callback after the handler is run. +* `afterCompletion(..)` -- callback after the complete request has finished. + +NOTE: For `@ResponseBody` and `ResponseEntity` controller methods, the response is written +and committed within the `HandlerAdapter`, before `postHandle` is called. That means it is +too late to change the response, such as to add an extra header. You can implement +`ResponseBodyAdvice` and declare it as an +xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice] bean or configure it +directly on `RequestMappingHandlerAdapter`. See xref:web/webmvc/mvc-config/interceptors.adoc[Interceptors] in the section on MVC configuration for examples of how to configure interceptors. You can also register them directly by using setters on individual `HandlerMapping` implementations. -`postHandle` method is less useful with `@ResponseBody` and `ResponseEntity` methods for -which the response is written and committed within the `HandlerAdapter` and before -`postHandle`. That means it is too late to make any changes to the response, such as adding -an extra header. For such scenarios, you can implement `ResponseBodyAdvice` and either -declare it as an xref:web/webmvc/mvc-controller/ann-advice.adoc[Controller Advice] bean or configure it directly on -`RequestMappingHandlerAdapter`. - - - +WARNING: Interceptors are not ideally suited as a security layer due to the potential for +a mismatch with annotated controller path matching. Generally, we recommend using Spring +Security, or alternatively a similar approach integrated with the Servlet filter chain, +and applied as early as possible. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java index d904cb6d254f..524e4505da52 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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,9 +30,9 @@ * *

      A HandlerInterceptor gets called before the appropriate HandlerAdapter * triggers the execution of the handler itself. This mechanism can be used - * for a large field of preprocessing aspects, e.g. for authorization checks, - * or common handler behavior like locale or theme changes. Its main purpose - * is to allow for factoring out repetitive handler code. + * for a large field of preprocessing aspects, or common handler behavior + * like locale or theme changes. Its main purpose is to allow for factoring + * out repetitive handler code. * *

      In an asynchronous processing scenario, the handler may be executed in a * separate thread while the main thread exits without rendering or invoking the @@ -63,6 +63,12 @@ * forms and GZIP compression. This typically shows when one needs to map the * filter to certain content types (e.g. images), or to all requests. * + *

      Note: Interceptors are not ideally suited as a security + * layer due to the potential for a mismatch with annotated controller path matching. + * Generally, we recommend using Spring Security, or alternatively a similar + * approach integrated with the Servlet filter chain, and applied as early as + * possible. + * * @author Juergen Hoeller * @since 20.06.2003 * @see HandlerExecutionChain#getInterceptors From e81c788274b6026758fb5bdabfcf7eae875e97d8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 14 May 2024 13:03:29 +0200 Subject: [PATCH 189/261] Accept ajc-compiled @Aspect classes for Spring AOP proxy usage AspectJExpressionPointcut leniently ignores unsupported expression. Closes gh-32793 --- .../aspectj/AspectJExpressionPointcut.java | 23 ++++++++----- .../AbstractAspectJAdvisorFactory.java | 32 ++---------------- .../AspectJExpressionPointcutTests.java | 33 +++++++++---------- 3 files changed, 31 insertions(+), 57 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index 51463440472c..d44801846bd2 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -169,25 +169,30 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override public ClassFilter getClassFilter() { - obtainPointcutExpression(); + checkExpression(); return this; } @Override public MethodMatcher getMethodMatcher() { - obtainPointcutExpression(); + checkExpression(); return this; } /** - * Check whether this pointcut is ready to match, - * lazily building the underlying AspectJ pointcut expression. + * Check whether this pointcut is ready to match. */ - private PointcutExpression obtainPointcutExpression() { + private void checkExpression() { if (getExpression() == null) { throw new IllegalStateException("Must set property 'expression' before attempting to match"); } + } + + /** + * Lazily build the underlying AspectJ pointcut expression. + */ + private PointcutExpression obtainPointcutExpression() { if (this.pointcutExpression == null) { this.pointcutClassLoader = determinePointcutClassLoader(); this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); @@ -264,10 +269,9 @@ public PointcutExpression getPointcutExpression() { @Override public boolean matches(Class targetClass) { - PointcutExpression pointcutExpression = obtainPointcutExpression(); try { try { - return pointcutExpression.couldMatchJoinPointsInType(targetClass); + return obtainPointcutExpression().couldMatchJoinPointsInType(targetClass); } catch (ReflectionWorldException ex) { logger.debug("PointcutExpression matching rejected target class - trying fallback expression", ex); @@ -278,6 +282,9 @@ public boolean matches(Class targetClass) { } } } + catch (IllegalArgumentException | IllegalStateException ex) { + throw ex; + } catch (Throwable ex) { logger.debug("PointcutExpression matching rejected target class", ex); } @@ -286,7 +293,6 @@ public boolean matches(Class targetClass) { @Override public boolean matches(Method method, Class targetClass, boolean hasIntroductions) { - obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Special handling for this, target, @this, @target, @annotation @@ -324,7 +330,6 @@ public boolean isRuntime() { @Override public boolean matches(Method method, Class targetClass, Object... args) { - obtainPointcutExpression(); ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target, diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java index bf2eb3e45056..6f0eef820701 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; -import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; import java.util.StringTokenizer; @@ -56,8 +55,6 @@ */ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory { - private static final String AJC_MAGIC = "ajc$"; - private static final Class[] ASPECTJ_ANNOTATION_CLASSES = new Class[] { Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class}; @@ -68,37 +65,11 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac protected final ParameterNameDiscoverer parameterNameDiscoverer = new AspectJAnnotationParameterNameDiscoverer(); - /** - * We consider something to be an AspectJ aspect suitable for use by the Spring AOP system - * if it has the @Aspect annotation, and was not compiled by ajc. The reason for this latter test - * is that aspects written in the code-style (AspectJ language) also have the annotation present - * when compiled by ajc with the -1.5 flag, yet they cannot be consumed by Spring AOP. - */ @Override public boolean isAspect(Class clazz) { - return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz)); - } - - private boolean hasAspectAnnotation(Class clazz) { return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); } - /** - * We need to detect this as "code-style" AspectJ aspects should not be - * interpreted by Spring AOP. - */ - private boolean compiledByAjc(Class clazz) { - // The AJTypeSystem goes to great lengths to provide a uniform appearance between code-style and - // annotation-style aspects. Therefore there is no 'clean' way to tell them apart. Here we rely on - // an implementation detail of the AspectJ compiler. - for (Field field : clazz.getDeclaredFields()) { - if (field.getName().startsWith(AJC_MAGIC)) { - return true; - } - } - return false; - } - @Override public void validate(Class aspectClass) throws AopConfigException { AjType ajType = AjTypeSystem.getAjType(aspectClass); @@ -115,6 +86,7 @@ public void validate(Class aspectClass) throws AopConfigException { } } + /** * Find and return the first AspectJ annotation on the given method * (there should only be one anyway...). diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index 0a2b5ab8f9ba..4602a1e8a9fe 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -23,8 +23,6 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; -import org.aspectj.weaver.tools.PointcutPrimitive; -import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import test.annotation.EmptySpringAnnotation; @@ -41,7 +39,6 @@ import org.springframework.beans.testfixture.beans.subpkg.DeepBean; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -174,25 +171,25 @@ private void testWithinPackage(boolean matchSubpackages) throws SecurityExceptio @Test public void testFriendlyErrorOnNoLocationClassMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(ITestBean.class)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getClassFilter().matches(ITestBean.class)) + .withMessageContaining("expression"); } @Test public void testFriendlyErrorOnNoLocation2ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(getAge, ITestBean.class)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class)) + .withMessageContaining("expression"); } @Test public void testFriendlyErrorOnNoLocation3ArgMatching() { AspectJExpressionPointcut pc = new AspectJExpressionPointcut(); - assertThatIllegalStateException().isThrownBy(() -> - pc.matches(getAge, ITestBean.class, (Object[]) null)) - .withMessageContaining("expression"); + assertThatIllegalStateException() + .isThrownBy(() -> pc.getMethodMatcher().matches(getAge, ITestBean.class, (Object[]) null)) + .withMessageContaining("expression"); } @@ -209,8 +206,10 @@ public void testMatchWithArgs() throws Exception { // not currently testable in a reliable fashion //assertDoesNotMatchStringClass(classFilter); - assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)).as("Should match with setSomeNumber with Double input").isTrue(); - assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)).as("Should not match setSomeNumber with Integer input").isFalse(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 12D)) + .as("Should match with setSomeNumber with Double input").isTrue(); + assertThat(methodMatcher.matches(setSomeNumber, TestBean.class, 11)) + .as("Should not match setSomeNumber with Integer input").isFalse(); assertThat(methodMatcher.matches(getAge, TestBean.class)).as("Should not match getAge").isFalse(); assertThat(methodMatcher.isRuntime()).as("Should be a runtime match").isTrue(); } @@ -245,7 +244,7 @@ public void testDynamicMatchingProxy() { @Test public void testInvalidExpression() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)"; - assertThatIllegalArgumentException().isThrownBy(getPointcut(expression)::getClassFilter); // call to getClassFilter forces resolution + assertThatIllegalArgumentException().isThrownBy(() -> getPointcut(expression).getClassFilter().matches(Object.class)); } private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) { @@ -275,9 +274,7 @@ private void assertMatchesTestBeanClass(ClassFilter classFilter) { @Test public void testWithUnsupportedPointcutPrimitive() { String expression = "call(int org.springframework.beans.testfixture.beans.TestBean.getAge())"; - assertThatExceptionOfType(UnsupportedPointcutPrimitiveException.class) - .isThrownBy(() -> getPointcut(expression).getClassFilter()) // call to getClassFilter forces resolution... - .satisfies(ex -> assertThat(ex.getUnsupportedPrimitive()).isEqualTo(PointcutPrimitive.CALL)); + assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } @Test From ee3e1591de7552046f8eec079cf02442fe63f949 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 14 May 2024 13:03:35 +0200 Subject: [PATCH 190/261] Polishing --- .../aop/aspectj/annotation/AspectMetadata.java | 16 +++++++++++----- .../util/MultiValueMapAdapter.java | 6 +++--- .../org/springframework/util/StringUtils.java | 11 ++++++----- .../rowset/ResultSetWrappingSqlRowSet.java | 4 ++-- .../jms/core/JmsMessagingTemplate.java | 7 +------ .../springframework/jms/core/JmsOperations.java | 14 +++++++------- .../core/AbstractMessageSendingTemplate.java | 5 ++--- .../org/springframework/http/ResponseEntity.java | 3 +-- .../HttpComponentsClientHttpRequest.java | 5 +---- .../servlet/support/AbstractFlashMapManager.java | 4 ++-- 10 files changed, 36 insertions(+), 39 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java index 969ec866eebb..a70aed625cc9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -124,10 +124,16 @@ public AspectMetadata(Class aspectClass, String aspectName) { * Extract contents from String of form {@code pertarget(contents)}. */ private String findPerClause(Class aspectClass) { - String str = aspectClass.getAnnotation(Aspect.class).value(); - int beginIndex = str.indexOf('(') + 1; - int endIndex = str.length() - 1; - return str.substring(beginIndex, endIndex); + Aspect ann = aspectClass.getAnnotation(Aspect.class); + if (ann == null) { + return ""; + } + String value = ann.value(); + int beginIndex = value.indexOf('('); + if (beginIndex < 0) { + return ""; + } + return value.substring(beginIndex + 1, value.length() - 1); } diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java index 8c158ecf0fc4..4c7c2f1d133c 100644 --- a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,7 @@ public MultiValueMapAdapter(Map> targetMap) { @Nullable public V getFirst(K key) { List values = this.targetMap.get(key); - return (values != null && !values.isEmpty() ? values.get(0) : null); + return (!CollectionUtils.isEmpty(values) ? values.get(0) : null); } @Override @@ -95,7 +95,7 @@ public void setAll(Map values) { public Map toSingleValueMap() { Map singleValueMap = CollectionUtils.newLinkedHashMap(this.targetMap.size()); this.targetMap.forEach((key, values) -> { - if (values != null && !values.isEmpty()) { + if (!CollectionUtils.isEmpty(values)) { singleValueMap.put(key, values.get(0)); } }); diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 8936c605bab0..93c6dc216168 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -123,7 +123,7 @@ public static boolean isEmpty(@Nullable Object str) { * @see #hasText(CharSequence) */ public static boolean hasLength(@Nullable CharSequence str) { - return (str != null && str.length() > 0); + return (str != null && !str.isEmpty()); // as of JDK 15 } /** @@ -791,6 +791,7 @@ public static boolean pathEquals(String path1, String path2) { * and {@code "0"} through {@code "9"} stay the same. *

    1. Special characters {@code "-"}, {@code "_"}, {@code "."}, and {@code "*"} stay the same.
    2. *
    3. A sequence "{@code %xy}" is interpreted as a hexadecimal representation of the character.
    4. + *
    5. For all other characters (including those already decoded), the output is undefined.
    6. * * @param source the encoded String * @param charset the character set @@ -839,7 +840,7 @@ public static String uriDecode(String source, Charset charset) { * the {@link Locale#toString} format as well as BCP 47 language tags as * specified by {@link Locale#forLanguageTag}. * @param localeValue the locale value: following either {@code Locale's} - * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as + * {@code toString()} format ("en", "en_UK", etc.), also accepting spaces as * separators (as an alternative to underscores), or BCP 47 (e.g. "en-UK") * @return a corresponding {@code Locale} instance, or {@code null} if none * @throws IllegalArgumentException in case of an invalid locale specification @@ -868,7 +869,7 @@ public static Locale parseLocale(String localeValue) { *

      Note: This delegate does not accept the BCP 47 language tag format. * Please use {@link #parseLocale} for lenient parsing of both formats. * @param localeString the locale {@code String}: following {@code Locale's} - * {@code toString()} format ("en", "en_UK", etc), also accepting spaces as + * {@code toString()} format ("en", "en_UK", etc.), also accepting spaces as * separators (as an alternative to underscores) * @return a corresponding {@code Locale} instance, or {@code null} if none * @throws IllegalArgumentException in case of an invalid locale specification @@ -876,7 +877,7 @@ public static Locale parseLocale(String localeValue) { @SuppressWarnings("deprecation") // for Locale constructors on JDK 19 @Nullable public static Locale parseLocaleString(String localeString) { - if (localeString.equals("")) { + if (localeString.isEmpty()) { return null; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java index d9b68de09bc1..56d3accb10d7 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,7 +32,7 @@ import org.springframework.util.CollectionUtils; /** - * The default implementation of Spring's {@link SqlRowSet} interface, wrapping a + * The common implementation of Spring's {@link SqlRowSet} interface, wrapping a * {@link java.sql.ResultSet}, catching any {@link SQLException SQLExceptions} and * translating them to a corresponding Spring {@link InvalidResultSetAccessException}. * diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java index af7e0e5e24ec..5c0aeb27b959 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -189,11 +189,6 @@ public void send(Message message) { } } - @Override - public void convertAndSend(Object payload) throws MessagingException { - convertAndSend(payload, null); - } - @Override public void convertAndSend(Object payload, @Nullable MessagePostProcessor postProcessor) throws MessagingException { Destination defaultDestination = getDefaultDestination(); diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java index 845be02a12e0..0aa7888819a1 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 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. @@ -290,7 +290,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor *

      This method should be used carefully, since it will block the thread * until the message becomes available or until the timeout value is exceeded. *

      This will only work with a default destination specified! - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -303,7 +303,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor *

      This method should be used carefully, since it will block the thread * until the message becomes available or until the timeout value is exceeded. * @param destination the destination to receive a message from - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -317,7 +317,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * until the message becomes available or until the timeout value is exceeded. * @param destinationName the name of the destination to send this message to * (to be resolved to an actual destination by a DestinationResolver) - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -332,7 +332,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor *

      This will only work with a default destination specified! * @param messageSelector the JMS message selector expression (or {@code null} if none). * See the JMS specification for a detailed definition of selector expressions. - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -347,7 +347,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @param destination the destination to receive a message from * @param messageSelector the JMS message selector expression (or {@code null} if none). * See the JMS specification for a detailed definition of selector expressions. - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable @@ -363,7 +363,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * (to be resolved to an actual destination by a DestinationResolver) * @param messageSelector the JMS message selector expression (or {@code null} if none). * See the JMS specification for a detailed definition of selector expressions. - * @return the message produced for the consumer or {@code null} if the timeout expires. + * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ @Nullable diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageSendingTemplate.java b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageSendingTemplate.java index b4cf36ae0097..f355b343216c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageSendingTemplate.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageSendingTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -168,8 +168,7 @@ protected Message doConvert(Object payload, @Nullable Map hea Map headersToUse = processHeadersToSend(headers); if (headersToUse != null) { - messageHeaders = (headersToUse instanceof MessageHeaders _messageHeaders ? - _messageHeaders : new MessageHeaders(headersToUse)); + messageHeaders = (headersToUse instanceof MessageHeaders mh ? mh : new MessageHeaders(headersToUse)); } MessageConverter converter = getMessageConverter(); 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 360f0321c334..c96065ab72ca 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -277,7 +277,6 @@ public static ResponseEntity of(Optional body) { */ public static HeadersBuilder of(ProblemDetail body) { return new DefaultBuilder(body.getStatus()) { - @SuppressWarnings("unchecked") @Override public ResponseEntity build() { diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java index 9a87c252abf6..596e649202e5 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -124,7 +124,6 @@ public Mono setComplete() { @Override protected void applyHeaders() { HttpHeaders headers = getHeaders(); - headers.entrySet() .stream() .filter(entry -> !HttpHeaders.CONTENT_LENGTH.equals(entry.getKey())) @@ -133,7 +132,6 @@ protected void applyHeaders() { if (!this.httpRequest.containsHeader(HttpHeaders.ACCEPT)) { this.httpRequest.addHeader(HttpHeaders.ACCEPT, MediaType.ALL_VALUE); } - this.contentLength = headers.getContentLength(); } @@ -144,7 +142,6 @@ protected void applyCookies() { } CookieStore cookieStore = this.context.getCookieStore(); - getCookies().values() .stream() .flatMap(Collection::stream) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java index 10b46f1923ba..38ee6c19df16 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -220,7 +220,7 @@ public final void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest reque @Nullable private String decodeAndNormalizePath(@Nullable String path, HttpServletRequest request) { - if (path != null && !path.isEmpty()) { + if (StringUtils.hasLength(path)) { path = getUrlPathHelper().decodeRequestString(request, path); if (path.charAt(0) != '/') { String requestUri = getUrlPathHelper().getRequestUri(request); From 4caf6bc5b818aa78316267e9d0305287512aecff Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 14 May 2024 13:45:38 +0200 Subject: [PATCH 191/261] Polishing --- .../src/main/java/org/springframework/util/StringUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index 93c6dc216168..52c2ca00bcfb 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -853,7 +853,7 @@ public static Locale parseLocale(String localeValue) { if (!localeValue.contains("_") && !localeValue.contains(" ")) { validateLocalePart(localeValue); Locale resolved = Locale.forLanguageTag(localeValue); - if (resolved.getLanguage().length() > 0) { + if (!resolved.getLanguage().isEmpty()) { return resolved; } } @@ -1182,7 +1182,7 @@ public static String[] tokenizeToStringArray( if (trimTokens) { token = token.trim(); } - if (!ignoreEmptyTokens || token.length() > 0) { + if (!ignoreEmptyTokens || !token.isEmpty()) { tokens.add(token); } } @@ -1244,7 +1244,7 @@ public static String[] delimitedListToStringArray( result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); pos = delPos + delimiter.length(); } - if (str.length() > 0 && pos <= str.length()) { + if (!str.isEmpty() && pos <= str.length()) { // Add rest of String, but not in case of empty input. result.add(deleteAny(str.substring(pos), charsToDelete)); } From 9fb36a5dcb82c06a482b3388f96eed4017f47b18 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 14 May 2024 22:45:33 +0200 Subject: [PATCH 192/261] Upgrade to Reactor 2022.0.19 Includes AspectJ 1.9.22.1, Mockito 5.12, plugin alignment with 6.1.x Closes gh-32787 --- build.gradle | 6 +++--- framework-platform/framework-platform.gradle | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index d402f8d9f165..5b6834135166 100644 --- a/build.gradle +++ b/build.gradle @@ -3,12 +3,12 @@ plugins { id 'io.freefair.aspectj' version '8.0.1' apply false // kotlinVersion is managed in gradle.properties id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false - id 'org.jetbrains.dokka' version '1.8.10' + id 'org.jetbrains.dokka' version '1.8.20' id 'org.unbroken-dome.xjc' version '2.0.0' apply false - id 'com.github.ben-manes.versions' version '0.49.0' + id 'com.github.ben-manes.versions' version '0.51.0' id 'com.github.johnrengelman.shadow' version '8.1.1' apply false id 'de.undercouch.download' version '5.4.0' - id 'me.champeau.jmh' version '0.7.1' apply false + id 'me.champeau.jmh' version '0.7.2' apply false } ext { diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 9ef10a88b8f0..f7a2d290517f 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.10.13")) api(platform("io.netty:netty-bom:4.1.109.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.18")) + api(platform("io.projectreactor:reactor-bom:2022.0.19")) api(platform("io.rsocket:rsocket-bom:1.1.3")) api(platform("org.apache.groovy:groovy-bom:4.0.21")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) @@ -19,7 +19,7 @@ dependencies { api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) api(platform("org.junit:junit-bom:5.9.3")) - api(platform("org.mockito:mockito-bom:5.11.0")) + api(platform("org.mockito:mockito-bom:5.12.0")) constraints { api("com.fasterxml:aalto-xml:1.3.2") @@ -103,9 +103,9 @@ dependencies { api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.15") api("org.apache.tomcat:tomcat-util:10.1.15") api("org.apache.tomcat:tomcat-websocket:10.1.15") - api("org.aspectj:aspectjrt:1.9.20.1") - api("org.aspectj:aspectjtools:1.9.20.1") - api("org.aspectj:aspectjweaver:1.9.20.1") + api("org.aspectj:aspectjrt:1.9.22.1") + api("org.aspectj:aspectjtools:1.9.22.1") + api("org.aspectj:aspectjweaver:1.9.22.1") api("org.assertj:assertj-core:3.24.2") api("org.awaitility:awaitility:4.2.0") api("org.bouncycastle:bcpkix-jdk18on:1.72") From c8c95e360fa4a41b40e152b7a77b59c669b7a02d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 15 May 2024 14:31:48 +0200 Subject: [PATCH 193/261] Polishing (aligned with 6.1.x) --- .../PathMatchingResourcePatternResolver.java | 226 ++++++++++-------- .../springframework/util/ResourceUtils.java | 25 +- 2 files changed, 144 insertions(+), 107 deletions(-) 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 4a074e015284..a9fe21195cac 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 @@ -71,18 +71,18 @@ /** * A {@link ResourcePatternResolver} implementation that is able to resolve a * specified resource location path into one or more matching Resources. - * The source path may be a simple path which has a one-to-one mapping to a - * target {@link org.springframework.core.io.Resource}, or alternatively - * may contain the special "{@code classpath*:}" prefix and/or - * internal Ant-style regular expressions (matched using Spring's - * {@link org.springframework.util.AntPathMatcher} utility). - * Both of the latter are effectively wildcards. * - *

      No Wildcards: + *

      The source path may be a simple path which has a one-to-one mapping to a + * target {@link org.springframework.core.io.Resource}, or alternatively may + * contain the special "{@code classpath*:}" prefix and/or internal Ant-style + * path patterns (matched using Spring's {@link AntPathMatcher} utility). Both + * of the latter are effectively wildcards. + * + *

      No Wildcards

      * *

      In the simple case, if the specified location path does not start with the - * {@code "classpath*:}" prefix, and does not contain a PathMatcher pattern, - * this resolver will simply return a single resource via a + * {@code "classpath*:}" prefix and does not contain a {@link PathMatcher} + * pattern, this resolver will simply return a single resource via a * {@code getResource()} call on the underlying {@code ResourceLoader}. * Examples are real URLs such as "{@code file:C:/context.xml}", pseudo-URLs * such as "{@code classpath:/context.xml}", and simple unprefixed paths @@ -90,14 +90,14 @@ * fashion specific to the underlying {@code ResourceLoader} (e.g. * {@code ServletContextResource} for a {@code WebApplicationContext}). * - *

      Ant-style Patterns: + *

      Ant-style Patterns

      * - *

      When the path location contains an Ant-style pattern, e.g.: + *

      When the path location contains an Ant-style pattern, for example: *

        * /WEB-INF/*-context.xml
      - * com/mycompany/**/applicationContext.xml
      + * com/example/**/applicationContext.xml
        * file:C:/some/path/*-context.xml
      - * classpath:com/mycompany/**/applicationContext.xml
      + * classpath:com/example/**/applicationContext.xml * the resolver follows a more complex but defined procedure to try to resolve * the wildcard. It produces a {@code Resource} for the path up to the last * non-wildcard segment and obtains a {@code URL} from it. If this URL is not a @@ -108,31 +108,31 @@ * {@code java.net.JarURLConnection} from it, or manually parses the jar URL, and * then traverses the contents of the jar file, to resolve the wildcards. * - *

      Implications on portability: + *

      Implications on Portability

      * *

      If the specified path is already a file URL (either explicitly, or * implicitly because the base {@code ResourceLoader} is a filesystem one), * then wildcarding is guaranteed to work in a completely portable fashion. * - *

      If the specified path is a classpath location, then the resolver must + *

      If the specified path is a class path location, then the resolver must * obtain the last non-wildcard path segment URL via a * {@code Classloader.getResource()} call. Since this is just a * node of the path (not the file at the end) it is actually undefined * (in the ClassLoader Javadocs) exactly what sort of URL is returned in * this case. In practice, it is usually a {@code java.io.File} representing - * the directory, where the classpath resource resolves to a filesystem - * location, or a jar URL of some sort, where the classpath resource resolves + * the directory, where the class path resource resolves to a filesystem + * location, or a jar URL of some sort, where the class path resource resolves * to a jar location. Still, there is a portability concern on this operation. * *

      If a jar URL is obtained for the last non-wildcard segment, the resolver * must be able to get a {@code java.net.JarURLConnection} from it, or - * manually parse the jar URL, to be able to walk the contents of the jar, - * and resolve the wildcard. This will work in most environments, but will + * manually parse the jar URL, to be able to walk the contents of the jar + * and resolve the wildcard. This will work in most environments but will * fail in others, and it is strongly recommended that the wildcard * resolution of resources coming from jars be thoroughly tested in your * specific environment before you rely on it. * - *

      {@code classpath*:} Prefix: + *

      {@code classpath*:} Prefix

      * *

      There is special support for retrieving multiple class path resources with * the same name, via the "{@code classpath*:}" prefix. For example, @@ -142,22 +142,22 @@ * at the same location within each jar file. Internally, this happens via a * {@code ClassLoader.getResources()} call, and is completely portable. * - *

      The "classpath*:" prefix can also be combined with a PathMatcher pattern in - * the rest of the location path, for example "classpath*:META-INF/*-beans.xml". - * In this case, the resolution strategy is fairly simple: a - * {@code ClassLoader.getResources()} call is used on the last non-wildcard - * path segment to get all the matching resources in the class loader hierarchy, - * and then off each resource the same PathMatcher resolution strategy described - * above is used for the wildcard sub pattern. + *

      The "{@code classpath*:}" prefix can also be combined with a {@code PathMatcher} + * pattern in the rest of the location path — for example, + * "{@code classpath*:META-INF/*-beans.xml"}. In this case, the resolution strategy + * is fairly simple: a {@code ClassLoader.getResources()} call is used on the last + * non-wildcard path segment to get all the matching resources in the class loader + * hierarchy, and then off each resource the same {@code PathMatcher} resolution + * strategy described above is used for the wildcard sub pattern. * - *

      Other notes: + *

      Other Notes

      * - *

      As of Spring Framework 6.0, if {@link #getResources(String)} is invoked - * with a location pattern using the "classpath*:" prefix it will first search + *

      As of Spring Framework 6.0, if {@link #getResources(String)} is invoked with + * a location pattern using the "{@code classpath*:}" prefix it will first search * all modules in the {@linkplain ModuleLayer#boot() boot layer}, excluding * {@linkplain ModuleFinder#ofSystem() system modules}. It will then search the - * classpath using {@link ClassLoader} APIs as described previously and return the - * combined results. Consequently, some of the limitations of classpath searches + * class path using {@link ClassLoader} APIs as described previously and return the + * combined results. Consequently, some of the limitations of class path searches * may not apply when applications are deployed as modules. * *

      WARNING: Note that "{@code classpath*:}" when combined with @@ -168,26 +168,26 @@ * root of expanded directories. This originates from a limitation in the JDK's * {@code ClassLoader.getResources()} method which only returns file system * locations for a passed-in empty String (indicating potential roots to search). - * This {@code ResourcePatternResolver} implementation is trying to mitigate the + * This {@code ResourcePatternResolver} implementation tries to mitigate the * jar root lookup limitation through {@link URLClassLoader} introspection and - * "java.class.path" manifest evaluation; however, without portability guarantees. + * "{@code java.class.path}" manifest evaluation; however, without portability + * guarantees. * - *

      WARNING: Ant-style patterns with "classpath:" resources are not - * guaranteed to find matching resources if the root package to search is available + *

      WARNING: Ant-style patterns with "{@code classpath:}" resources are not + * guaranteed to find matching resources if the base package to search is available * in multiple class path locations. This is because a resource such as *

      - *     com/mycompany/package1/service-context.xml
      - * 
      - * may be in only one location, but when a path such as + * com/example/package1/service-context.xml + * may exist in only one class path location, but when a location pattern such as *
      - *     classpath:com/mycompany/**/service-context.xml
      - * 
      + * classpath:com/example/**/service-context.xml * is used to try to resolve it, the resolver will work off the (first) URL - * returned by {@code getResource("com/mycompany");}. If this base package node - * exists in multiple classloader locations, the actual end resource may not be - * underneath. Therefore, preferably, use "{@code classpath*:}" with the same - * Ant-style pattern in such a case, which will search all class path - * locations that contain the root package. + * returned by {@code getResource("com/example")}. If the {@code com/example} base + * package node exists in multiple class path locations, the actual desired resource + * may not be present under the {@code com/example} base package in the first URL. + * Therefore, preferably, use "{@code classpath*:}" with the same Ant-style pattern + * in such a case, which will search all class path locations that contain + * the base package. * * @author Juergen Hoeller * @author Colin Sampaleanu @@ -249,19 +249,21 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol /** - * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. + * Create a {@code PathMatchingResourcePatternResolver} with a + * {@link DefaultResourceLoader}. *

      ClassLoader access will happen via the thread context class loader. - * @see org.springframework.core.io.DefaultResourceLoader + * @see DefaultResourceLoader */ public PathMatchingResourcePatternResolver() { this.resourceLoader = new DefaultResourceLoader(); } /** - * Create a new PathMatchingResourcePatternResolver. + * Create a {@code PathMatchingResourcePatternResolver} with the supplied + * {@link ResourceLoader}. *

      ClassLoader access will happen via the thread context class loader. - * @param resourceLoader the ResourceLoader to load root directories and - * actual resources with + * @param resourceLoader the {@code ResourceLoader} to load root directories + * and actual resources with */ public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { Assert.notNull(resourceLoader, "ResourceLoader must not be null"); @@ -269,8 +271,9 @@ public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { } /** - * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader. - * @param classLoader the ClassLoader to load classpath resources with, + * Create a {@code PathMatchingResourcePatternResolver} with a + * {@link DefaultResourceLoader} and the supplied {@link ClassLoader}. + * @param classLoader the ClassLoader to load class path resources with, * or {@code null} for using the thread context class loader * at the time of actual resource access * @see org.springframework.core.io.DefaultResourceLoader @@ -281,7 +284,7 @@ public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) { /** - * Return the ResourceLoader that this pattern resolver works with. + * Return the {@link ResourceLoader} that this pattern resolver works with. */ public ResourceLoader getResourceLoader() { return this.resourceLoader; @@ -294,9 +297,10 @@ public ClassLoader getClassLoader() { } /** - * Set the PathMatcher implementation to use for this - * resource pattern resolver. Default is AntPathMatcher. - * @see org.springframework.util.AntPathMatcher + * Set the {@link PathMatcher} implementation to use for this + * resource pattern resolver. + *

      Default is {@link AntPathMatcher}. + * @see AntPathMatcher */ public void setPathMatcher(PathMatcher pathMatcher) { Assert.notNull(pathMatcher, "PathMatcher must not be null"); @@ -304,7 +308,7 @@ public void setPathMatcher(PathMatcher pathMatcher) { } /** - * Return the PathMatcher that this resource pattern resolver uses. + * Return the {@link PathMatcher} that this resource pattern resolver uses. */ public PathMatcher getPathMatcher() { return this.pathMatcher; @@ -353,8 +357,8 @@ public Resource[] getResources(String locationPattern) throws IOException { /** * Find all class location resources with the given location via the ClassLoader. - * Delegates to {@link #doFindAllClassPathResources(String)}. - * @param location the absolute path within the classpath + *

      Delegates to {@link #doFindAllClassPathResources(String)}. + * @param location the absolute path within the class path * @return the result as Resource array * @throws IOException in case of I/O errors * @see java.lang.ClassLoader#getResources @@ -364,15 +368,16 @@ protected Resource[] findAllClassPathResources(String location) throws IOExcepti String path = stripLeadingSlash(location); Set result = doFindAllClassPathResources(path); if (logger.isTraceEnabled()) { - logger.trace("Resolved classpath location [" + path + "] to resources " + result); + logger.trace("Resolved class path location [" + path + "] to resources " + result); } return result.toArray(new Resource[0]); } /** - * Find all class location resources with the given path via the ClassLoader. - * Called by {@link #findAllClassPathResources(String)}. - * @param path the absolute path within the classpath (never a leading slash) + * Find all class path resources with the given path via the configured + * {@link #getClassLoader() ClassLoader}. + *

      Called by {@link #findAllClassPathResources(String)}. + * @param path the absolute path within the class path (never a leading slash) * @return a mutable Set of matching Resource instances * @since 4.1.1 */ @@ -386,20 +391,21 @@ protected Set doFindAllClassPathResources(String path) throws IOExcept } if (!StringUtils.hasLength(path)) { // The above result is likely to be incomplete, i.e. only containing file system references. - // We need to have pointers to each of the jar files on the classpath as well... + // We need to have pointers to each of the jar files on the class path as well... addAllClassLoaderJarRoots(cl, result); } return result; } /** - * Convert the given URL as returned from the ClassLoader into a {@link Resource}, - * applying to path lookups without a pattern ({@link #findAllClassPathResources}). + * Convert the given URL as returned from the configured + * {@link #getClassLoader() ClassLoader} into a {@link Resource}, applying + * to path lookups without a pattern (see {@link #findAllClassPathResources}). *

      As of 6.0.5, the default implementation creates a {@link FileSystemResource} * in case of the "file" protocol or a {@link UrlResource} otherwise, matching - * the outcome of pattern-based classpath traversal in the same resource layout, + * the outcome of pattern-based class path traversal in the same resource layout, * as well as matching the outcome of module path searches. - * @param url a URL as returned from the ClassLoader + * @param url a URL as returned from the configured ClassLoader * @return the corresponding Resource object * @see java.lang.ClassLoader#getResources * @see #doFindAllClassPathResources @@ -422,8 +428,8 @@ protected Resource convertClassLoaderURL(URL url) { } /** - * Search all {@link URLClassLoader} URLs for jar file references and add them to the - * given set of resources in the form of pointers to the root of the jar file content. + * Search all {@link URLClassLoader} URLs for jar file references and add each to the + * given set of resources in the form of a pointer to the root of the jar file content. * @param classLoader the ClassLoader to search (including its ancestors) * @param result the set of resources to add jar roots to * @since 4.1.1 @@ -457,7 +463,7 @@ protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set< } if (classLoader == ClassLoader.getSystemClassLoader()) { - // "java.class.path" manifest evaluation... + // JAR "Class-Path" manifest header evaluation... addClassPathManifestEntries(result); } @@ -476,16 +482,17 @@ protected void addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set< } /** - * Determine jar file references from the "java.class.path." manifest property and add them - * to the given set of resources in the form of pointers to the root of the jar file content. + * Determine jar file references from {@code Class-Path} manifest entries (which + * are added to the {@code java.class.path} JVM system property by the system + * class loader) and add each to the given set of resources in the form of + * a pointer to the root of the jar file content. * @param result the set of resources to add jar roots to * @since 4.3 */ protected void addClassPathManifestEntries(Set result) { try { String javaClassPathProperty = System.getProperty("java.class.path"); - for (String path : StringUtils.delimitedListToStringArray( - javaClassPathProperty, System.getProperty("path.separator"))) { + for (String path : StringUtils.delimitedListToStringArray(javaClassPathProperty, File.pathSeparator)) { try { String filePath = new File(path).getAbsolutePath(); int prefixIndex = filePath.indexOf(':'); @@ -499,7 +506,7 @@ protected void addClassPathManifestEntries(Set result) { // 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! + // Potentially overlapping with URLClassLoader.getURLs() result in addAllClassLoaderJarRoots(). if (!result.contains(jarResource) && !hasDuplicate(filePath, result) && jarResource.exists()) { result.add(jarResource); } @@ -543,14 +550,18 @@ private boolean hasDuplicate(String filePath, Set result) { } /** - * Find all resources that match the given location pattern via the - * Ant-style PathMatcher. Supports resources in OSGi bundles, JBoss VFS, - * jar files, zip files, and file systems. + * Find all resources that match the given location pattern via the Ant-style + * {@link #getPathMatcher() PathMatcher}. + *

      Supports resources in OSGi bundles, JBoss VFS, jar files, zip files, + * and file systems. * @param locationPattern the location pattern to match * @return the result as Resource array * @throws IOException in case of I/O errors - * @see #doFindPathMatchingJarResources - * @see #doFindPathMatchingFileResources + * @see #determineRootDir(String) + * @see #resolveRootDirResource(Resource) + * @see #isJarResource(Resource) + * @see #doFindPathMatchingJarResources(Resource, URL, String) + * @see #doFindPathMatchingFileResources(Resource, String) * @see org.springframework.util.PathMatcher */ protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { @@ -607,29 +618,39 @@ protected String determineRootDir(String location) { } /** - * Resolve the specified resource for path matching. - *

      By default, Equinox OSGi "bundleresource:" / "bundleentry:" URL will be - * resolved into a standard jar file URL that be traversed using Spring's - * standard jar file traversal algorithm. For any preceding custom resolution, - * override this method and replace the resource handle accordingly. + * Resolve the supplied root directory resource for path matching. + *

      By default, {@link #findPathMatchingResources(String)} resolves Equinox + * OSGi "bundleresource:" and "bundleentry:" URLs into standard jar file URLs + * that will be traversed using Spring's standard jar file traversal algorithm. + *

      For any custom resolution, override this template method and replace the + * supplied resource handle accordingly. + *

      The default implementation of this method returns the supplied resource + * unmodified. * @param original the resource to resolve - * @return the resolved resource (may be identical to the passed-in resource) + * @return the resolved resource (may be identical to the supplied resource) * @throws IOException in case of resolution failure + * @see #findPathMatchingResources(String) */ protected Resource resolveRootDirResource(Resource original) throws IOException { return original; } /** - * Return whether the given resource handle indicates a jar resource - * that the {@link #doFindPathMatchingJarResources} method can handle. - *

      By default, the URL protocols "jar", "zip", "vfszip, and "wsjar" - * will be treated as jar resources. This template method allows for - * detecting further kinds of jar-like resources, e.g. through - * {@code instanceof} checks on the resource handle type. - * @param resource the resource handle to check - * (usually the root directory to start path matching from) - * @see #doFindPathMatchingJarResources + * Determine if the given resource handle indicates a jar resource that the + * {@link #doFindPathMatchingJarResources} method can handle. + *

      {@link #findPathMatchingResources(String)} delegates to + * {@link ResourceUtils#isJarURL(URL)} to determine whether the given URL + * points to a resource in a jar file, and only invokes this method as a fallback. + *

      This template method therefore allows for detecting further kinds of + * jar-like resources — for example, via {@code instanceof} checks on + * the resource handle type. + *

      The default implementation of this method returns {@code false}. + * @param resource the resource handle to check (usually the root directory + * to start path matching from) + * @return {@code true} if the given resource handle indicates a jar resource + * @throws IOException in case of I/O errors + * @see #findPathMatchingResources(String) + * @see #doFindPathMatchingJarResources(Resource, URL, String) * @see org.springframework.util.ResourceUtils#isJarURL */ protected boolean isJarResource(Resource resource) throws IOException { @@ -638,7 +659,7 @@ protected boolean isJarResource(Resource resource) throws IOException { /** * Find all resources in jar files that match the given location pattern - * via the Ant-style PathMatcher. + * via the Ant-style {@link #getPathMatcher() PathMatcher}. * @param rootDirResource the root directory as Resource * @param rootDirUrl the pre-resolved root directory URL * @param subPattern the sub pattern to match (below the root directory) @@ -691,7 +712,7 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, } catch (ZipException ex) { if (logger.isDebugEnabled()) { - logger.debug("Skipping invalid jar classpath entry [" + urlFile + "]"); + logger.debug("Skipping invalid jar class path entry [" + urlFile + "]"); } return Collections.emptySet(); } @@ -746,7 +767,8 @@ protected JarFile getJarFile(String jarFileUrl) throws IOException { /** * Find all resources in the file system of the supplied root directory that - * match the given location sub pattern via the Ant-style PathMatcher. + * match the given location sub pattern via the Ant-style {@link #getPathMatcher() + * PathMatcher}. * @param rootDirResource the root directory as a Resource * @param subPattern the sub pattern to match (below the root directory) * @return a mutable Set of matching Resource instances @@ -924,8 +946,8 @@ private Resource findResource(ModuleReader moduleReader, String name) { } /** - * If it's a "file:" URI, use FileSystemResource to avoid duplicates - * for the same path discovered via class-path scanning. + * If it's a "file:" URI, use {@link FileSystemResource} to avoid duplicates + * for the same path discovered via class path scanning. */ private Resource convertModuleSystemURI(URI uri) { return (ResourceUtils.URL_PROTOCOL_FILE.equals(uri.getScheme()) ? diff --git a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java index af5b6746b960..6909a65c54d6 100644 --- a/spring-core/src/main/java/org/springframework/util/ResourceUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ResourceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -100,6 +100,7 @@ public abstract class ResourceUtils { * @return whether the location qualifies as a URL * @see #CLASSPATH_URL_PREFIX * @see java.net.URL + * @see #toURL(String) */ public static boolean isUrl(@Nullable String resourceLocation) { if (resourceLocation == null) { @@ -125,6 +126,7 @@ public static boolean isUrl(@Nullable String resourceLocation) { * "classpath:" pseudo URL, a "file:" URL, or a plain file path * @return a corresponding URL object * @throws FileNotFoundException if the resource cannot be resolved to a URL + * @see #toURL(String) */ public static URL getURL(String resourceLocation) throws FileNotFoundException { Assert.notNull(resourceLocation, "Resource location must not be null"); @@ -165,6 +167,7 @@ public static URL getURL(String resourceLocation) throws FileNotFoundException { * @return a corresponding File object * @throws FileNotFoundException if the resource cannot be resolved to * a file in the file system + * @see #getFile(URL) */ public static File getFile(String resourceLocation) throws FileNotFoundException { Assert.notNull(resourceLocation, "Resource location must not be null"); @@ -196,6 +199,7 @@ public static File getFile(String resourceLocation) throws FileNotFoundException * @return a corresponding File object * @throws FileNotFoundException if the URL cannot be resolved to * a file in the file system + * @see #getFile(URL, String) */ public static File getFile(URL resourceUrl) throws FileNotFoundException { return getFile(resourceUrl, "URL"); @@ -236,6 +240,7 @@ public static File getFile(URL resourceUrl, String description) throws FileNotFo * @throws FileNotFoundException if the URL cannot be resolved to * a file in the file system * @since 2.5 + * @see #getFile(URI, String) */ public static File getFile(URI resourceUri) throws FileNotFoundException { return getFile(resourceUri, "URI"); @@ -267,6 +272,7 @@ public static File getFile(URI resourceUri, String description) throws FileNotFo * i.e. has protocol "file", "vfsfile" or "vfs". * @param url the URL to check * @return whether the URL has been identified as a file system URL + * @see #isJarURL(URL) */ public static boolean isFileURL(URL url) { String protocol = url.getProtocol(); @@ -275,10 +281,12 @@ public static boolean isFileURL(URL url) { } /** - * Determine whether the given URL points to a resource in a jar file. - * i.e. has protocol "jar", "war, ""zip", "vfszip" or "wsjar". + * Determine whether the given URL points to a resource in a jar file + * — for example, whether the URL has protocol "jar", "war, "zip", + * "vfszip", or "wsjar". * @param url the URL to check * @return whether the URL has been identified as a JAR URL + * @see #isJarFileURL(URL) */ public static boolean isJarURL(URL url) { String protocol = url.getProtocol(); @@ -293,6 +301,7 @@ public static boolean isJarURL(URL url) { * @param url the URL to check * @return whether the URL has been identified as a JAR file URL * @since 4.1 + * @see #extractJarFileURL(URL) */ public static boolean isJarFileURL(URL url) { return (URL_PROTOCOL_FILE.equals(url.getProtocol()) && @@ -305,6 +314,7 @@ public static boolean isJarFileURL(URL url) { * @param jarUrl the original URL * @return the URL for the actual jar file * @throws MalformedURLException if no valid jar file URL could be extracted + * @see #extractArchiveURL(URL) */ public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException { String urlFile = jarUrl.getFile(); @@ -366,6 +376,7 @@ public static URL extractArchiveURL(URL jarUrl) throws MalformedURLException { * @return the URI instance * @throws URISyntaxException if the URL wasn't a valid URI * @see java.net.URL#toURI() + * @see #toURI(String) */ public static URI toURI(URL url) throws URISyntaxException { return toURI(url.toString()); @@ -377,6 +388,7 @@ public static URI toURI(URL url) throws URISyntaxException { * @param location the location String to convert into a URI instance * @return the URI instance * @throws URISyntaxException if the location wasn't a valid URI + * @see #toURI(URL) */ public static URI toURI(String location) throws URISyntaxException { return new URI(StringUtils.replace(location, " ", "%20")); @@ -389,6 +401,7 @@ public static URI toURI(String location) throws URISyntaxException { * @return the URL instance * @throws MalformedURLException if the location wasn't a valid URL * @since 6.0 + * @see java.net.URL#URL(String) */ public static URL toURL(String location) throws MalformedURLException { // Equivalent without java.net.URL constructor - for building on JDK 20+ @@ -414,6 +427,7 @@ public static URL toURL(String location) throws MalformedURLException { * @return the relative URL instance * @throws MalformedURLException if the end result is not a valid URL * @since 6.0 + * @see java.net.URL#URL(URL, String) */ public static URL toRelativeURL(URL root, String relativePath) throws MalformedURLException { // # can appear in filenames, java.net.URL should not treat it as a fragment @@ -429,9 +443,10 @@ public static URL toRelativeURL(URL root, String relativePath) throws MalformedU /** * Set the {@link URLConnection#setUseCaches "useCaches"} flag on the - * given connection, preferring {@code false} but leaving the - * flag at {@code true} for JNLP based resources. + * given connection, preferring {@code false} but leaving the flag at + * {@code true} for JNLP based resources. * @param con the URLConnection to set the flag on + * @see URLConnection#setUseCaches */ public static void useCachesIfNecessary(URLConnection con) { con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP")); From ea208dc3048b358f014d1e16a3722d6b73f41e13 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 15 May 2024 20:16:39 +0100 Subject: [PATCH 194/261] Polishing contribution Closes gh-32799 --- .../http/client/reactive/JdkClientHttpRequest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java index faf65dc9ed79..2295d4ba2cb9 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +37,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; /** * {@link ClientHttpRequest} for the Java {@link HttpClient}. @@ -108,7 +109,11 @@ protected void applyHeaders() { @Override protected void applyCookies() { - this.builder.header(HttpHeaders.COOKIE, getCookies().values().stream() + MultiValueMap cookies = getCookies(); + if (cookies.isEmpty()) { + return; + } + this.builder.header(HttpHeaders.COOKIE, cookies.values().stream() .flatMap(List::stream).map(HttpCookie::toString).collect(Collectors.joining(";"))); } From c374c46cdc7f9080785b9e8979dd8bbef9a08bb9 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 16 May 2024 08:24:45 +0000 Subject: [PATCH 195/261] Next development version (v6.0.21-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index aeac3c788d62..7feb238244d6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.20-SNAPSHOT +version=6.0.21-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 97e12bd0e84a4f5bc5689609cc8d5dfddeba3cce Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 17 May 2024 12:27:59 +0200 Subject: [PATCH 196/261] Defensively catch and log pointcut parsing exceptions Closes gh-32838 See gh-32793 (cherry picked from commit 617833bec97bdee369af01462b68f8878eb1b155) --- .../aop/aspectj/AspectJExpressionPointcut.java | 14 ++++++++++++-- .../aspectj/AspectJExpressionPointcutTests.java | 4 ++-- .../AutoProxyWithCodeStyleAspectsTests.java | 5 +++-- .../aop/aspectj/autoproxy/ajcAutoproxyTests.xml | 11 ++++++++--- .../aop/aspectj/OverloadedAdviceTests.java | 9 +++------ .../aop/aspectj/OverloadedAdviceTests.xml | 2 ++ 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index d44801846bd2..72dc0edda1f0 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -41,6 +41,7 @@ import org.aspectj.weaver.tools.PointcutParser; import org.aspectj.weaver.tools.PointcutPrimitive; import org.aspectj.weaver.tools.ShadowMatch; +import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionAwareMethodMatcher; @@ -114,6 +115,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut @Nullable private transient PointcutExpression pointcutExpression; + private transient boolean pointcutParsingFailed = false; + private transient Map shadowMatchCache = new ConcurrentHashMap<>(32); @@ -269,6 +272,10 @@ public PointcutExpression getPointcutExpression() { @Override public boolean matches(Class targetClass) { + if (this.pointcutParsingFailed) { + return false; + } + try { try { return obtainPointcutExpression().couldMatchJoinPointsInType(targetClass); @@ -282,8 +289,11 @@ public boolean matches(Class targetClass) { } } } - catch (IllegalArgumentException | IllegalStateException ex) { - throw ex; + catch (IllegalArgumentException | IllegalStateException | UnsupportedPointcutPrimitiveException ex) { + this.pointcutParsingFailed = true; + if (logger.isDebugEnabled()) { + logger.debug("Pointcut parser rejected expression [" + getExpression() + "]: " + ex); + } } catch (Throwable ex) { logger.debug("PointcutExpression matching rejected target class", ex); diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java index 4602a1e8a9fe..c754f3d8a85a 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/AspectJExpressionPointcutTests.java @@ -39,13 +39,13 @@ import org.springframework.beans.testfixture.beans.subpkg.DeepBean; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * @author Rob Harrop * @author Rod Johnson * @author Chris Beams + * @author Juergen Hoeller */ public class AspectJExpressionPointcutTests { @@ -244,7 +244,7 @@ public void testDynamicMatchingProxy() { @Test public void testInvalidExpression() { String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)"; - assertThatIllegalArgumentException().isThrownBy(() -> getPointcut(expression).getClassFilter().matches(Object.class)); + assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse(); } private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) { diff --git a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java index 43947fa29c29..0e62726089d2 100644 --- a/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java +++ b/spring-aspects/src/test/java/org/springframework/aop/aspectj/autoproxy/AutoProxyWithCodeStyleAspectsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2024 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. @@ -22,12 +22,13 @@ /** * @author Adrian Colyer + * @author Juergen Hoeller */ public class AutoProxyWithCodeStyleAspectsTests { @Test @SuppressWarnings("resource") - public void noAutoproxyingOfAjcCompiledAspects() { + public void noAutoProxyingOfAjcCompiledAspects() { new ClassPathXmlApplicationContext("org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml"); } diff --git a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml index 6be707bf51dd..63d6e15591d4 100644 --- a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml +++ b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml @@ -2,16 +2,21 @@ + http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd"> - + + + + + diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java index 91597c9ab85c..257b6bf37580 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/OverloadedAdviceTests.java @@ -28,17 +28,14 @@ * * @author Adrian Colyer * @author Chris Beams + * @author Juergen Hoeller */ class OverloadedAdviceTests { @Test @SuppressWarnings("resource") - void testExceptionOnConfigParsingWithMismatchedAdviceMethod() { - assertThatExceptionOfType(BeanCreationException.class) - .isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass())) - .havingRootCause() - .isInstanceOf(IllegalArgumentException.class) - .as("invalidAbsoluteTypeName should be detected by AJ").withMessageContaining("invalidAbsoluteTypeName"); + void testConfigParsingWithMismatchedAdviceMethod() { + new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()); } @Test diff --git a/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml index df9bfadc8ebc..ae175f39a197 100644 --- a/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml +++ b/spring-context/src/test/resources/org/springframework/aop/aspectj/OverloadedAdviceTests.xml @@ -18,4 +18,6 @@ + + \ No newline at end of file From a0f07af375fa817898dcc76a266cc7f1f0b07428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 20 May 2024 13:32:04 +0200 Subject: [PATCH 197/261] Avoid reader on empty content to be shared by multiple requests This commit avoids several instances of MockHttpServletRequest to have a common reader for empty content as closing it will have an unwanted side effect on the others. Closes gh-32848 --- .../mock/web/MockHttpServletRequest.java | 5 +---- .../mock/web/MockHttpServletRequestTests.java | 11 +++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java index 25032db6a2df..d7f60f936b35 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -100,9 +100,6 @@ public class MockHttpServletRequest implements HttpServletRequest { private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); - private static final BufferedReader EMPTY_BUFFERED_READER = - new BufferedReader(new StringReader("")); - /** * Date formats as specified in the HTTP RFC. * @see Section 7.1.1.1 of RFC 7231 @@ -736,7 +733,7 @@ else if (this.inputStream != null) { this.reader = new BufferedReader(sourceReader); } else { - this.reader = EMPTY_BUFFERED_READER; + this.reader = new BufferedReader(new StringReader("")); } return this.reader; } diff --git a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java index 9aec6036fbd1..b999557a8985 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletRequestTests.java @@ -93,6 +93,17 @@ void readEmptyInputStreamWorksAcrossRequests() throws IOException { secondRequest.getInputStream().close(); } + @Test // gh-32820 + void readEmptyReaderWorksAcrossRequests() throws IOException { + MockHttpServletRequest firstRequest = new MockHttpServletRequest(); + firstRequest.getReader().read(new char[256]); + firstRequest.getReader().close(); + + MockHttpServletRequest secondRequest = new MockHttpServletRequest(); + secondRequest.getReader().read(new char[256]); + secondRequest.getReader().close(); + } + @Test void setContentAndGetReader() throws IOException { byte[] bytes = "body".getBytes(Charset.defaultCharset()); From 4c9de3cbbddddb4791ffdef12d6dfae9ff6dc674 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 21 May 2024 11:16:19 +0200 Subject: [PATCH 198/261] Avoid creation of SAXParserFactory for every read operation Includes JAXBContext locking revision (avoiding synchronization) and consistent treatment of DocumentBuilderFactory (in terms of caching as well as locking). Closes gh-32851 (cherry picked from commit a4c2f291d986ccf988f37af5466265a7f534c16a) --- .../oxm/jaxb/Jaxb2Marshaller.java | 49 +++++++++--- .../oxm/support/AbstractMarshaller.java | 46 ++++++----- .../Jaxb2RootElementHttpMessageConverter.java | 23 ++++-- .../xml/SourceHttpMessageConverter.java | 78 ++++++++++++------- 4 files changed, 135 insertions(+), 61 deletions(-) diff --git a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java index 2f69913413ea..336ce8d213b0 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/Jaxb2Marshaller.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,6 +37,8 @@ import java.util.Date; import java.util.Map; import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import javax.xml.XMLConstants; import javax.xml.datatype.Duration; @@ -192,7 +194,7 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi @Nullable private ClassLoader beanClassLoader; - private final Object jaxbContextMonitor = new Object(); + private final Lock jaxbContextLock = new ReentrantLock(); @Nullable private volatile JAXBContext jaxbContext; @@ -204,6 +206,12 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi private boolean processExternalEntities = false; + @Nullable + private volatile SAXParserFactory schemaParserFactory; + + @Nullable + private volatile SAXParserFactory sourceParserFactory; + /** * Set multiple JAXB context paths. The given array of context paths gets @@ -426,6 +434,7 @@ public void setMappedClass(Class mappedClass) { */ public void setSupportDtd(boolean supportDtd) { this.supportDtd = supportDtd; + this.sourceParserFactory = null; } /** @@ -450,6 +459,7 @@ public void setProcessExternalEntities(boolean processExternalEntities) { if (processExternalEntities) { this.supportDtd = true; } + this.sourceParserFactory = null; } /** @@ -497,7 +507,9 @@ public JAXBContext getJaxbContext() { if (context != null) { return context; } - synchronized (this.jaxbContextMonitor) { + + this.jaxbContextLock.lock(); + try { context = this.jaxbContext; if (context == null) { try { @@ -521,6 +533,9 @@ else if (!ObjectUtils.isEmpty(this.packagesToScan)) { } return context; } + finally { + this.jaxbContextLock.unlock(); + } } private JAXBContext createJaxbContextFromContextPath(String contextPath) throws JAXBException { @@ -587,17 +602,24 @@ private Schema loadSchema(Resource[] resources, String schemaLanguage) throws IO Assert.notEmpty(resources, "No resources given"); Assert.hasLength(schemaLanguage, "No schema language provided"); Source[] schemaSources = new Source[resources.length]; - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - saxParserFactory.setNamespaceAware(true); - saxParserFactory.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + + SAXParserFactory saxParserFactory = this.schemaParserFactory; + if (saxParserFactory == null) { + saxParserFactory = SAXParserFactory.newInstance(); + saxParserFactory.setNamespaceAware(true); + saxParserFactory.setFeature("http://xml.org/sax/features/namespace-prefixes", true); + this.schemaParserFactory = saxParserFactory; + } SAXParser saxParser = saxParserFactory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); + for (int i = 0; i < resources.length; i++) { Resource resource = resources[i]; Assert.isTrue(resource != null && resource.exists(), () -> "Resource does not exist: " + resource); InputSource inputSource = SaxResourceUtils.createInputSource(resource); schemaSources[i] = new SAXSource(xmlReader, inputSource); } + SchemaFactory schemaFactory = SchemaFactory.newInstance(schemaLanguage); if (this.schemaResourceResolver != null) { schemaFactory.setResourceResolver(this.schemaResourceResolver); @@ -886,11 +908,16 @@ else if (streamSource.getReader() != null) { try { if (xmlReader == null) { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - saxParserFactory.setNamespaceAware(true); - saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); - String name = "http://xml.org/sax/features/external-general-entities"; - saxParserFactory.setFeature(name, isProcessExternalEntities()); + SAXParserFactory saxParserFactory = this.sourceParserFactory; + if (saxParserFactory == null) { + saxParserFactory = SAXParserFactory.newInstance(); + saxParserFactory.setNamespaceAware(true); + saxParserFactory.setFeature( + "http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); + saxParserFactory.setFeature( + "http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); + this.sourceParserFactory = saxParserFactory; + } SAXParser saxParser = saxParserFactory.newSAXParser(); xmlReader = saxParser.getXMLReader(); } diff --git a/spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java index 7c6182c4a77d..a3925acb5039 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -83,9 +83,10 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller { private boolean processExternalEntities = false; @Nullable - private DocumentBuilderFactory documentBuilderFactory; + private volatile DocumentBuilderFactory documentBuilderFactory; - private final Object documentBuilderFactoryMonitor = new Object(); + @Nullable + private volatile SAXParserFactory saxParserFactory; /** @@ -94,6 +95,8 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller { */ public void setSupportDtd(boolean supportDtd) { this.supportDtd = supportDtd; + this.documentBuilderFactory = null; + this.saxParserFactory = null; } /** @@ -118,6 +121,8 @@ public void setProcessExternalEntities(boolean processExternalEntities) { if (processExternalEntities) { this.supportDtd = true; } + this.documentBuilderFactory = null; + this.saxParserFactory = null; } /** @@ -137,14 +142,13 @@ public boolean isProcessExternalEntities() { */ protected Document buildDocument() { try { - DocumentBuilder documentBuilder; - synchronized (this.documentBuilderFactoryMonitor) { - if (this.documentBuilderFactory == null) { - this.documentBuilderFactory = createDocumentBuilderFactory(); - } - documentBuilder = createDocumentBuilder(this.documentBuilderFactory); + DocumentBuilderFactory builderFactory = this.documentBuilderFactory; + if (builderFactory == null) { + builderFactory = createDocumentBuilderFactory(); + this.documentBuilderFactory = builderFactory; } - return documentBuilder.newDocument(); + DocumentBuilder builder = createDocumentBuilder(builderFactory); + return builder.newDocument(); } catch (ParserConfigurationException ex) { throw new UnmarshallingFailureException("Could not create document placeholder: " + ex.getMessage(), ex); @@ -179,11 +183,11 @@ protected DocumentBuilderFactory createDocumentBuilderFactory() throws ParserCon protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory) throws ParserConfigurationException { - DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + DocumentBuilder builder = factory.newDocumentBuilder(); if (!isProcessExternalEntities()) { - documentBuilder.setEntityResolver(NO_OP_ENTITY_RESOLVER); + builder.setEntityResolver(NO_OP_ENTITY_RESOLVER); } - return documentBuilder; + return builder; } /** @@ -193,11 +197,17 @@ protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory) * @throws ParserConfigurationException if thrown by JAXP methods */ protected XMLReader createXmlReader() throws SAXException, ParserConfigurationException { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - saxParserFactory.setNamespaceAware(true); - saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); - saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); - SAXParser saxParser = saxParserFactory.newSAXParser(); + SAXParserFactory parserFactory = this.saxParserFactory; + if (parserFactory == null) { + parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + parserFactory.setFeature( + "http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); + parserFactory.setFeature( + "http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); + this.saxParserFactory = parserFactory; + } + SAXParser saxParser = parserFactory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); if (!isProcessExternalEntities()) { xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER); diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java index 5100061cacea..d156a08ba162 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -61,6 +61,7 @@ * @author Arjen Poutsma * @author Sebastien Deleuze * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.0 * @see MarshallingHttpMessageConverter */ @@ -70,6 +71,9 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa private boolean processExternalEntities = false; + @Nullable + private volatile SAXParserFactory sourceParserFactory; + /** * Indicate whether DTD parsing should be supported. @@ -77,6 +81,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa */ public void setSupportDtd(boolean supportDtd) { this.supportDtd = supportDtd; + this.sourceParserFactory = null; } /** @@ -97,6 +102,7 @@ public void setProcessExternalEntities(boolean processExternalEntities) { if (processExternalEntities) { this.supportDtd = true; } + this.sourceParserFactory = null; } /** @@ -156,11 +162,16 @@ protected Source processSource(Source source) { if (source instanceof StreamSource streamSource) { InputSource inputSource = new InputSource(streamSource.getInputStream()); try { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - saxParserFactory.setNamespaceAware(true); - saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); - String featureName = "http://xml.org/sax/features/external-general-entities"; - saxParserFactory.setFeature(featureName, isProcessExternalEntities()); + SAXParserFactory saxParserFactory = this.sourceParserFactory; + if (saxParserFactory == null) { + saxParserFactory = SAXParserFactory.newInstance(); + saxParserFactory.setNamespaceAware(true); + saxParserFactory.setFeature( + "http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); + saxParserFactory.setFeature( + "http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); + this.sourceParserFactory = saxParserFactory; + } SAXParser saxParser = saxParserFactory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); if (!isProcessExternalEntities()) { diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java index 40d4b4de422a..0f3adee9a4b9 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,6 +63,7 @@ * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.0 * @param the converted object type */ @@ -75,11 +76,7 @@ public class SourceHttpMessageConverter extends AbstractHttpMe (publicID, systemID, base, ns) -> InputStream.nullInputStream(); private static final Set> SUPPORTED_CLASSES = Set.of( - DOMSource.class, - SAXSource.class, - StAXSource.class, - StreamSource.class, - Source.class); + DOMSource.class, SAXSource.class, StAXSource.class, StreamSource.class, Source.class); private final TransformerFactory transformerFactory = TransformerFactory.newInstance(); @@ -88,10 +85,19 @@ public class SourceHttpMessageConverter extends AbstractHttpMe private boolean processExternalEntities = false; + @Nullable + private volatile DocumentBuilderFactory documentBuilderFactory; + + @Nullable + private volatile SAXParserFactory saxParserFactory; + + @Nullable + private volatile XMLInputFactory xmlInputFactory; + /** * Sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} - * to {@code text/xml} and {@code application/xml}, and {@code application/*-xml}. + * to {@code text/xml} and {@code application/xml}, and {@code application/*+xml}. */ public SourceHttpMessageConverter() { super(MediaType.APPLICATION_XML, MediaType.TEXT_XML, new MediaType("application", "*+xml")); @@ -104,6 +110,9 @@ public SourceHttpMessageConverter() { */ public void setSupportDtd(boolean supportDtd) { this.supportDtd = supportDtd; + this.documentBuilderFactory = null; + this.saxParserFactory = null; + this.xmlInputFactory = null; } /** @@ -124,6 +133,9 @@ public void setProcessExternalEntities(boolean processExternalEntities) { if (processExternalEntities) { this.supportDtd = true; } + this.documentBuilderFactory = null; + this.saxParserFactory = null; + this.xmlInputFactory = null; } /** @@ -165,17 +177,21 @@ else if (StreamSource.class == clazz || Source.class == clazz) { private DOMSource readDOMSource(InputStream body, HttpInputMessage inputMessage) throws IOException { try { - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setNamespaceAware(true); - documentBuilderFactory.setFeature( - "http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); - documentBuilderFactory.setFeature( - "http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); - DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + DocumentBuilderFactory builderFactory = this.documentBuilderFactory; + if (builderFactory == null) { + builderFactory = DocumentBuilderFactory.newInstance(); + builderFactory.setNamespaceAware(true); + builderFactory.setFeature( + "http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); + builderFactory.setFeature( + "http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); + this.documentBuilderFactory = builderFactory; + } + DocumentBuilder builder = builderFactory.newDocumentBuilder(); if (!isProcessExternalEntities()) { - documentBuilder.setEntityResolver(NO_OP_ENTITY_RESOLVER); + builder.setEntityResolver(NO_OP_ENTITY_RESOLVER); } - Document document = documentBuilder.parse(body); + Document document = builder.parse(body); return new DOMSource(document); } catch (NullPointerException ex) { @@ -197,11 +213,17 @@ private DOMSource readDOMSource(InputStream body, HttpInputMessage inputMessage) private SAXSource readSAXSource(InputStream body, HttpInputMessage inputMessage) throws IOException { try { - SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); - saxParserFactory.setNamespaceAware(true); - saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); - saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); - SAXParser saxParser = saxParserFactory.newSAXParser(); + SAXParserFactory parserFactory = this.saxParserFactory; + if (parserFactory == null) { + parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + parserFactory.setFeature( + "http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd()); + parserFactory.setFeature( + "http://xml.org/sax/features/external-general-entities", isProcessExternalEntities()); + this.saxParserFactory = parserFactory; + } + SAXParser saxParser = parserFactory.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); if (!isProcessExternalEntities()) { xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER); @@ -217,11 +239,15 @@ private SAXSource readSAXSource(InputStream body, HttpInputMessage inputMessage) private Source readStAXSource(InputStream body, HttpInputMessage inputMessage) { try { - XMLInputFactory inputFactory = XMLInputFactory.newInstance(); - inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, isSupportDtd()); - inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, isProcessExternalEntities()); - if (!isProcessExternalEntities()) { - inputFactory.setXMLResolver(NO_OP_XML_RESOLVER); + XMLInputFactory inputFactory = this.xmlInputFactory; + if (inputFactory == null) { + inputFactory = XMLInputFactory.newInstance(); + inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, isSupportDtd()); + inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, isProcessExternalEntities()); + if (!isProcessExternalEntities()) { + inputFactory.setXMLResolver(NO_OP_XML_RESOLVER); + } + this.xmlInputFactory = inputFactory; } XMLStreamReader streamReader = inputFactory.createXMLStreamReader(body); return new StAXSource(streamReader); From 2b8a1faeaac3763aa82d34890827899305e57f19 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 21 May 2024 11:16:25 +0200 Subject: [PATCH 199/261] Polishing (cherry picked from commit 65e1337d35cf7aa398798b8a7a8034518943940e) --- ...SpringConfiguredWithAutoProxyingTests.java | 6 +++++- .../PersistenceUnitReader.java | 20 ++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java index a02bcad6793b..220780971182 100644 --- a/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java +++ b/spring-aspects/src/test/java/org/springframework/beans/factory/aspectj/SpringConfiguredWithAutoProxyingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2024 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,6 +20,10 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; +/** + * @author Ramnivas Laddad + * @author Juergen Hoeller + */ public class SpringConfiguredWithAutoProxyingTests { @Test diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java index 00a167f8d5ca..ddd74f19233e 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -154,7 +154,7 @@ public SpringPersistenceUnitInfo[] readPersistenceUnitInfos(String[] persistence /** * Validate the given stream and return a valid DOM document for parsing. */ - protected Document buildDocument(ErrorHandler handler, InputStream stream) + Document buildDocument(ErrorHandler handler, InputStream stream) throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); @@ -168,9 +168,7 @@ protected Document buildDocument(ErrorHandler handler, InputStream stream) /** * Parse the validated document and add entries to the given unit info list. */ - protected List parseDocument( - Resource resource, Document document, List infos) throws IOException { - + void parseDocument(Resource resource, Document document, List infos) throws IOException { Element persistence = document.getDocumentElement(); String version = persistence.getAttribute(PERSISTENCE_VERSION); URL rootUrl = determinePersistenceUnitRootUrl(resource); @@ -179,14 +177,12 @@ protected List parseDocument( for (Element unit : units) { infos.add(parsePersistenceUnitInfo(unit, version, rootUrl)); } - - return infos; } /** * Parse the unit info DOM element. */ - protected SpringPersistenceUnitInfo parsePersistenceUnitInfo( + SpringPersistenceUnitInfo parsePersistenceUnitInfo( Element persistenceUnit, String version, @Nullable URL rootUrl) throws IOException { SpringPersistenceUnitInfo unitInfo = new SpringPersistenceUnitInfo(); @@ -253,7 +249,7 @@ protected SpringPersistenceUnitInfo parsePersistenceUnitInfo( /** * Parse the {@code property} XML elements. */ - protected void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { Element propRoot = DomUtils.getChildElementByTagName(persistenceUnit, PROPERTIES); if (propRoot == null) { return; @@ -269,7 +265,7 @@ protected void parseProperties(Element persistenceUnit, SpringPersistenceUnitInf /** * Parse the {@code class} XML elements. */ - protected void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { List classes = DomUtils.getChildElementsByTagName(persistenceUnit, MANAGED_CLASS_NAME); for (Element element : classes) { String value = DomUtils.getTextValue(element).trim(); @@ -282,7 +278,7 @@ protected void parseManagedClasses(Element persistenceUnit, SpringPersistenceUni /** * Parse the {@code mapping-file} XML elements. */ - protected void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { + void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) { List files = DomUtils.getChildElementsByTagName(persistenceUnit, MAPPING_FILE_NAME); for (Element element : files) { String value = DomUtils.getTextValue(element).trim(); @@ -295,7 +291,7 @@ protected void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitI /** * Parse the {@code jar-file} XML elements. */ - protected void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException { + void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException { List jars = DomUtils.getChildElementsByTagName(persistenceUnit, JAR_FILE_URL); for (Element element : jars) { String value = DomUtils.getTextValue(element).trim(); From 84a5a8a61e236ccd2eb2fa78483a2de7c2e9e18a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 21 May 2024 17:39:06 +0200 Subject: [PATCH 200/261] Default fallback parsing for UTC without milliseconds Closes gh-32856 (cherry picked from commit fee17e11ba6668a5392c0db28c79d2d75be10c3d) --- .../format/datetime/DateFormatter.java | 33 ++++++++++-- .../format/datetime/DateFormatterTests.java | 53 +++++++++++++------ 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java index 06a95b7bee5a..e4185c3b0859 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -22,8 +22,10 @@ import java.util.Collections; import java.util.Date; import java.util.EnumMap; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.TimeZone; import org.springframework.format.Formatter; @@ -35,9 +37,14 @@ /** * A formatter for {@link java.util.Date} types. + * *

      Supports the configuration of an explicit date time pattern, timezone, * locale, and fallback date time patterns for lenient parsing. * + *

      Common ISO patterns for UTC instants are applied at millisecond precision. + * Note that {@link org.springframework.format.datetime.standard.InstantFormatter} + * is recommended for flexible UTC parsing into a {@link java.time.Instant} instead. + * * @author Keith Donald * @author Juergen Hoeller * @author Phillip Webb @@ -49,15 +56,23 @@ public class DateFormatter implements Formatter { private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); - // We use an EnumMap instead of Map.of(...) since the former provides better performance. private static final Map ISO_PATTERNS; + private static final Map ISO_FALLBACK_PATTERNS; + static { + // We use an EnumMap instead of Map.of(...) since the former provides better performance. Map formats = new EnumMap<>(ISO.class); formats.put(ISO.DATE, "yyyy-MM-dd"); formats.put(ISO.TIME, "HH:mm:ss.SSSXXX"); formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); ISO_PATTERNS = Collections.unmodifiableMap(formats); + + // Fallback format for the time part without milliseconds. + Map fallbackFormats = new EnumMap<>(ISO.class); + fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX"); + fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX"); + ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats); } @@ -202,8 +217,16 @@ public Date parse(String text, Locale locale) throws ParseException { return getDateFormat(locale).parse(text); } catch (ParseException ex) { + Set fallbackPatterns = new LinkedHashSet<>(); + String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso); + if (isoPattern != null) { + fallbackPatterns.add(isoPattern); + } if (!ObjectUtils.isEmpty(this.fallbackPatterns)) { - for (String pattern : this.fallbackPatterns) { + Collections.addAll(fallbackPatterns, this.fallbackPatterns); + } + if (!fallbackPatterns.isEmpty()) { + for (String pattern : fallbackPatterns) { try { DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale)); // Align timezone for parsing format with printing format if ISO is set. @@ -221,8 +244,8 @@ public Date parse(String text, Locale locale) throws ParseException { } if (this.source != null) { ParseException parseException = new ParseException( - String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), - ex.getErrorOffset()); + String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source), + ex.getErrorOffset()); parseException.initCause(ex); throw parseException; } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java index d085e4d622fa..f74f36edbd95 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -35,75 +35,83 @@ * * @author Keith Donald * @author Phillip Webb + * @author Juergen Hoeller */ -public class DateFormatterTests { +class DateFormatterTests { private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); @Test - public void shouldPrintAndParseDefault() throws Exception { + void shouldPrintAndParseDefault() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseFromPattern() throws ParseException { + void shouldPrintAndParseFromPattern() throws ParseException { DateFormatter formatter = new DateFormatter("yyyy-MM-dd"); formatter.setTimeZone(UTC); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseShort() throws Exception { + void shouldPrintAndParseShort() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.SHORT); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09"); assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseMedium() throws Exception { + void shouldPrintAndParseMedium() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.MEDIUM); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseLong() throws Exception { + void shouldPrintAndParseLong() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.LONG); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009"); assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseFull() throws Exception { + void shouldPrintAndParseFull() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.FULL); + Date date = getDate(2009, Calendar.JUNE, 1); assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009"); assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date); } @Test - public void shouldPrintAndParseISODate() throws Exception { + void shouldPrintAndParseIsoDate() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.DATE); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); assertThat(formatter.parse("2009-6-01", Locale.US)) @@ -111,45 +119,56 @@ public void shouldPrintAndParseISODate() throws Exception { } @Test - public void shouldPrintAndParseISOTime() throws Exception { + void shouldPrintAndParseIsoTime() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.TIME); + Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z"); assertThat(formatter.parse("14:23:05.003Z", Locale.US)) .isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3)); + + date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 0); + assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.000Z"); + assertThat(formatter.parse("14:23:05Z", Locale.US)) + .isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 0).toInstant()); } @Test - public void shouldPrintAndParseISODateTime() throws Exception { + void shouldPrintAndParseIsoDateTime() throws Exception { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setIso(ISO.DATE_TIME); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z"); assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date); + + date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 0); + assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.000Z"); + assertThat(formatter.parse("2009-06-01T14:23:05Z", Locale.US)).isEqualTo(date.toInstant()); } @Test - public void shouldThrowOnUnsupportedStylePattern() throws Exception { + void shouldThrowOnUnsupportedStylePattern() { DateFormatter formatter = new DateFormatter(); formatter.setStylePattern("OO"); - assertThatIllegalStateException().isThrownBy(() -> - formatter.parse("2009", Locale.US)) - .withMessageContaining("Unsupported style pattern 'OO'"); + + assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US)) + .withMessageContaining("Unsupported style pattern 'OO'"); } @Test - public void shouldUseCorrectOrder() throws Exception { + void shouldUseCorrectOrder() { DateFormatter formatter = new DateFormatter(); formatter.setTimeZone(UTC); formatter.setStyle(DateFormat.SHORT); formatter.setStylePattern("L-"); formatter.setIso(ISO.DATE_TIME); formatter.setPattern("yyyy"); - Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); + Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3); assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009"); formatter.setPattern(""); From 33d3496a162f8f734ee3e5e971d919dd3ea271d8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 21 May 2024 17:39:11 +0200 Subject: [PATCH 201/261] Polishing (cherry picked from commit 20dea0dae2725fbf371ff720fceb82e99d5fdd41) --- .../datetime/standard/InstantFormatter.java | 6 +++--- .../format/datetime/DateFormattingTests.java | 4 ++-- .../standard/DateTimeFormattingTests.java | 14 +++++++------- .../standard/InstantFormatterTests.java | 19 +++++++++---------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 456c0ad09090..06ff8213de57 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -41,12 +41,12 @@ public class InstantFormatter implements Formatter { @Override public Instant parse(String text, Locale locale) throws ParseException { - if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) { + if (!text.isEmpty() && Character.isAlphabetic(text.charAt(0))) { // assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT" return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text)); } else { - // assuming UTC instant a la "2007-12-03T10:15:30.00Z" + // assuming UTC instant a la "2007-12-03T10:15:30.000Z" return Instant.parse(text); } } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java index 2ea58f5e06a7..6f7751545e37 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -52,7 +52,7 @@ * @author Juergen Hoeller * @author Sam Brannen */ -public class DateFormattingTests { +class DateFormattingTests { private final FormattingConversionService conversionService = new FormattingConversionService(); diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java index 392fdd61c6e5..037aab46009a 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -512,7 +512,7 @@ void testBindYearMonth() { } @Test - public void testBindYearMonthAnnotatedPattern() { + void testBindYearMonthAnnotatedPattern() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("yearMonthAnnotatedPattern", "12/2007"); binder.bind(propertyValues); @@ -531,7 +531,7 @@ void testBindMonthDay() { } @Test - public void testBindMonthDayAnnotatedPattern() { + void testBindMonthDayAnnotatedPattern() { MutablePropertyValues propertyValues = new MutablePropertyValues(); propertyValues.add("monthDayAnnotatedPattern", "1/3"); binder.bind(propertyValues); @@ -631,10 +631,10 @@ public static class DateTimeBean { @DateTimeFormat(style = "M-") private LocalDate styleLocalDate; - @DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" }) + @DateTimeFormat(style = "S-", fallbackPatterns = {"yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd"}) private LocalDate styleLocalDateWithFallbackPatterns; - @DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" }) + @DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = {"M/d/yy", "yyyyMMdd", "yyyy.MM.dd"}) private LocalDate patternLocalDateWithFallbackPatterns; private LocalTime localTime; @@ -642,7 +642,7 @@ public static class DateTimeBean { @DateTimeFormat(style = "-M") private LocalTime styleLocalTime; - @DateTimeFormat(style = "-M", fallbackPatterns = { "HH:mm:ss", "HH:mm"}) + @DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"}) private LocalTime styleLocalTimeWithFallbackPatterns; private LocalDateTime localDateTime; @@ -662,7 +662,7 @@ public static class DateTimeBean { @DateTimeFormat(iso = ISO.DATE_TIME) private LocalDateTime isoLocalDateTime; - @DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = { "yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"}) + @DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = {"yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"}) private LocalDateTime isoLocalDateTimeWithFallbackPatterns; private Instant instant; diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java index 16ba2cbd5e5c..bd4058fdcae1 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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.text.ParseException; import java.time.Instant; import java.time.format.DateTimeFormatter; +import java.util.Locale; import java.util.Random; import java.util.stream.Stream; @@ -49,13 +50,12 @@ class InstantFormatterTests { private final InstantFormatter instantFormatter = new InstantFormatter(); + @ParameterizedTest @ArgumentsSource(ISOSerializedInstantProvider.class) void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException { Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from); - - Instant actual = instantFormatter.parse(input, null); - + Instant actual = instantFormatter.parse(input, Locale.US); assertThat(actual).isEqualTo(expected); } @@ -63,9 +63,7 @@ void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String in @ArgumentsSource(RFC1123SerializedInstantProvider.class) void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException { Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from); - - Instant actual = instantFormatter.parse(input, null); - + Instant actual = instantFormatter.parse(input, Locale.US); assertThat(actual).isEqualTo(expected); } @@ -73,12 +71,11 @@ void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(Strin @ArgumentsSource(RandomInstantProvider.class) void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) { String expected = DateTimeFormatter.ISO_INSTANT.format(input); - - String actual = instantFormatter.print(input, null); - + String actual = instantFormatter.print(input, Locale.US); assertThat(actual).isEqualTo(expected); } + private static class RandomInstantProvider implements ArgumentsProvider { private static final long DATA_SET_SIZE = 10; @@ -100,6 +97,7 @@ Stream randomInstantStream(Instant min, Instant max) { } } + private static class ISOSerializedInstantProvider extends RandomInstantProvider { @Override @@ -108,6 +106,7 @@ Stream provideArguments() { } } + private static class RFC1123SerializedInstantProvider extends RandomInstantProvider { // RFC-1123 supports only 4-digit years From 0ca47e5e0335d560d7316a89bb71fff4df393566 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 21 May 2024 18:24:04 +0200 Subject: [PATCH 202/261] Polishing --- .../standard/DateTimeFormattingTests.java | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java index 037aab46009a..0ac712f0c7f2 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -274,9 +274,7 @@ void testBindLocalDateTime() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value) - .startsWith("10/31/09") - .endsWith("12:00 PM"); + assertThat(value).startsWith("10/31/09").endsWith("12:00 PM"); } @Test @@ -286,9 +284,7 @@ void testBindLocalDateTimeWithISO() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value) - .startsWith("10/31/09") - .endsWith("12:00 PM"); + assertThat(value).startsWith("10/31/09").endsWith("12:00 PM"); } @Test @@ -298,9 +294,7 @@ void testBindLocalDateTimeAnnotated() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("styleLocalDateTime").toString(); - assertThat(value) - .startsWith("Oct 31, 2009") - .endsWith("12:00:00 PM"); + assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM"); } @Test @@ -310,9 +304,7 @@ void testBindLocalDateTimeFromJavaUtilCalendar() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value) - .startsWith("10/31/09") - .endsWith("12:00 PM"); + assertThat(value).startsWith("10/31/09").endsWith("12:00 PM"); } @Test @@ -325,9 +317,7 @@ void testBindDateTimeWithSpecificStyle() { binder.bind(propertyValues); assertThat(binder.getBindingResult().getErrorCount()).isZero(); String value = binder.getBindingResult().getFieldValue("localDateTime").toString(); - assertThat(value) - .startsWith("Oct 31, 2009") - .endsWith("12:00:00 PM"); + assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM"); } @Test @@ -540,6 +530,7 @@ void testBindMonthDayAnnotatedPattern() { assertThat(binder.getBindingResult().getRawFieldValue("monthDayAnnotatedPattern")).isEqualTo(MonthDay.parse("--01-03")); } + @Nested class FallbackPatternTests { @@ -690,7 +681,6 @@ public static class DateTimeBean { private final List children = new ArrayList<>(); - public LocalDate getLocalDate() { return this.localDate; } From e73d68b0a8682fc0a1b2f06feb9c9fc7af3a90ba Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 22 May 2024 10:07:31 +0200 Subject: [PATCH 203/261] Select most specific advice method in case of override Closes gh-32865 (cherry picked from commit ea596aa211ea17b814099a73bb5a091a9af1cadd) --- .../ReflectiveAspectJAdvisorFactory.java | 27 +++++++++-------- .../AbstractAspectJAdvisorFactoryTests.java | 30 ++++++++++--------- 2 files changed, 31 insertions(+), 26 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 4e2f5c3bd801..90cc3880cb84 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -50,6 +50,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConvertingComparator; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; @@ -133,17 +134,19 @@ public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstan List advisors = new ArrayList<>(); for (Method method : getAdvisorMethods(aspectClass)) { - // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect - // to getAdvisor(...) to represent the "current position" in the declared methods list. - // However, since Java 7 the "current position" is not valid since the JDK no longer - // returns declared methods in the order in which they are declared in the source code. - // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods - // discovered via reflection in order to support reliable advice ordering across JVM launches. - // Specifically, a value of 0 aligns with the default value used in - // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). - Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); - if (advisor != null) { - advisors.add(advisor); + if (method.equals(ClassUtils.getMostSpecificMethod(method, aspectClass))) { + // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect + // to getAdvisor(...) to represent the "current position" in the declared methods list. + // However, since Java 7 the "current position" is not valid since the JDK no longer + // returns declared methods in the order in which they are declared in the source code. + // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods + // discovered via reflection in order to support reliable advice ordering across JVM launches. + // Specifically, a value of 0 aligns with the default value used in + // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); + if (advisor != null) { + advisors.add(advisor); + } } } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java index 978cd0495407..02d968212d53 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,7 +38,6 @@ import org.aspectj.lang.annotation.DeclarePrecedence; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.aop.Advisor; @@ -84,15 +83,15 @@ abstract class AbstractAspectJAdvisorFactoryTests { @Test void rejectsPerCflowAspect() { assertThatExceptionOfType(AopConfigException.class) - .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean"))) - .withMessageContaining("PERCFLOW"); + .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean"))) + .withMessageContaining("PERCFLOW"); } @Test void rejectsPerCflowBelowAspect() { assertThatExceptionOfType(AopConfigException.class) - .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) - .withMessageContaining("PERCFLOWBELOW"); + .isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean"))) + .withMessageContaining("PERCFLOWBELOW"); } @Test @@ -363,7 +362,7 @@ void introductionOnTargetImplementingInterface() { assertThat(lockable.locked()).as("Already locked").isTrue(); lockable.lock(); assertThat(lockable.locked()).as("Real target ignores locking").isTrue(); - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> lockable.unlock()); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(lockable::unlock); } @Test @@ -389,9 +388,7 @@ void introductionBasedOnAnnotationMatch() { // gh-9980 assertThat(lockable.locked()).isTrue(); } - // TODO: Why does this test fail? It hasn't been run before, so it maybe never actually passed... @Test - @Disabled void introductionWithArgumentBinding() { TestBean target = new TestBean(); @@ -648,7 +645,7 @@ void getAge() { static class NamedPointcutAspectWithFQN { @SuppressWarnings("unused") - private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); @Around("org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.CommonPointcuts.getAge()()") int changeReturnValue(ProceedingJoinPoint pjp) { @@ -765,7 +762,7 @@ Object echo(Object obj) throws Exception { @Aspect - class DoublingAspect { + static class DoublingAspect { @Around("execution(* getAge())") public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { @@ -773,8 +770,14 @@ public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { } } + @Aspect - class IncrementingAspect extends DoublingAspect { + static class IncrementingAspect extends DoublingAspect { + + @Override + public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable { + return ((int) pjp.proceed()) * 2; + } @Around("execution(* getAge())") public int incrementAge(ProceedingJoinPoint pjp) throws Throwable { @@ -783,7 +786,6 @@ public int incrementAge(ProceedingJoinPoint pjp) throws Throwable { } - @Aspect private static class InvocationTrackingAspect { @@ -1083,7 +1085,7 @@ class PerThisAspect { // Just to check that this doesn't cause problems with introduction processing @SuppressWarnings("unused") - private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); + private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean(); @Around("execution(int *.getAge())") int returnCountAsAge() { From d068f5a4c698769303fb2e5220269b68b076f64d Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 21 May 2024 21:49:24 -0500 Subject: [PATCH 204/261] Modernize Antora Build - Use same playbook as docs-build - Use Env Variables to cause partial build (same as docs-build) - Use package.json so that dependencies can be updated with dependabot --- .gitignore | 2 ++ framework-docs/antora-playbook.yml | 39 ++++++++++++++++++++++++++++ framework-docs/framework-docs.gradle | 23 +++------------- framework-docs/package.json | 10 +++++++ 4 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 framework-docs/antora-playbook.yml create mode 100644 framework-docs/package.json diff --git a/.gitignore b/.gitignore index 14252754d456..549d5756e164 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ atlassian-ide-plugin.xml .vscode/ cached-antora-playbook.yml + +node_modules diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml new file mode 100644 index 000000000000..9fc2d93216e3 --- /dev/null +++ b/framework-docs/antora-playbook.yml @@ -0,0 +1,39 @@ +antora: + extensions: + - require: '@springio/antora-extensions' + root_component_name: 'framework' +site: + title: Spring Framework + url: https://docs.spring.io/spring-framework/reference + robots: allow +git: + ensure_git_suffix: false +content: + sources: + - url: https://github.com/spring-projects/spring-framework + # Refname matching: + # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ + branches: ['main', '{6..9}.+({0..9}).x'] + tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})'] + start_path: framework-docs +asciidoc: + extensions: + - '@asciidoctor/tabs' + - '@springio/asciidoctor-extensions' + - '@springio/asciidoctor-extensions/include-code-extension' + attributes: + page-stackoverflow-url: https://stackoverflow.com/questions/tagged/spring + page-pagination: '' + hide-uri-scheme: '@' + tabs-sync-option: '@' + include-java: 'example$docs-src/main/java/org/springframework/docs' +urls: + latest_version_segment_strategy: redirect:to + latest_version_segment: '' + redirect_facility: httpd +runtime: + log: + failure_level: warn +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 56c68c1c7125..38785029861d 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -10,27 +10,10 @@ apply from: "${rootDir}/gradle/publications.gradle" antora { - version = '3.2.0-alpha.2' - playbook = 'cached-antora-playbook.yml' - playbookProvider { - repository = 'spring-projects/spring-framework' - branch = 'docs-build' - path = 'lib/antora/templates/per-branch-antora-playbook.yml' - checkLocalBranch = true - } - options = ['--clean', '--stacktrace'] + options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] environment = [ - 'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691', - 'ALGOLIA_APP_ID': '244V8V9FGG', - 'ALGOLIA_INDEX_NAME': 'framework-docs' - ] - dependencies = [ - '@antora/atlas-extension': '1.0.0-alpha.1', - '@antora/collector-extension': '1.0.0-alpha.3', - '@asciidoctor/tabs': '1.0.0-beta.3', - '@opendevise/antora-release-line-extension': '1.0.0-alpha.2', - '@springio/antora-extensions': '1.3.0', - '@springio/asciidoctor-extensions': '1.0.0-alpha.9' + 'BUILD_REFNAME': 'HEAD', + 'BUILD_VERSION': project.version, ] } diff --git a/framework-docs/package.json b/framework-docs/package.json new file mode 100644 index 000000000000..c3570e2f8a62 --- /dev/null +++ b/framework-docs/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "antora": "3.2.0-alpha.4", + "@antora/atlas-extension": "1.0.0-alpha.2", + "@antora/collector-extension": "1.0.0-alpha.3", + "@asciidoctor/tabs": "1.0.0-beta.6", + "@springio/antora-extensions": "1.11.1", + "@springio/asciidoctor-extensions": "1.0.0-alpha.10" + } +} From c993615f988f1871583bd1e8809d12ee643e6d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 22 May 2024 14:40:25 +0200 Subject: [PATCH 205/261] Remove outdated Javadoc links Closes gh-32873 --- build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5b6834135166..a9ce6eceb7af 100644 --- a/build.gradle +++ b/build.gradle @@ -112,8 +112,6 @@ configure([rootProject] + javaProjects) { project -> ext.javadocLinks = [ "https://docs.oracle.com/en/java/javase/17/docs/api/", "https://jakarta.ee/specifications/platform/9/apidocs/", - "https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ and weblogic.* packages - "https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/", // org.jboss.resource.* "https://docs.jboss.org/hibernate/orm/5.6/javadocs/", "https://eclipse.dev/aspectj/doc/released/aspectj5rt-api", "https://www.quartz-scheduler.org/api/2.3.0/", From 093e6a8e8d1b0eb61dbdf022c2dc629e814b1f2c Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Wed, 22 May 2024 16:07:26 +0000 Subject: [PATCH 206/261] Next development version (v6.0.22-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7feb238244d6..a40d4261e3d0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.21-SNAPSHOT +version=6.0.22-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From e12440a7af07c16d46177fc9b6290dfecaf12a04 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 May 2024 17:07:51 +0200 Subject: [PATCH 207/261] Defensive handling of incompatible advice methods This covers AspectJ transaction and caching aspects when encountered by Spring AOP. Closes gh-32882 See gh-32793 (cherry picked from commit 6d7cd9c7dc94dc55af9a8c4fba3171fedf5931b7) --- .../annotation/ReflectiveAspectJAdvisorFactory.java | 12 ++++++++++-- .../aop/aspectj/autoproxy/ajcAutoproxyTests.xml | 6 ++++++ 2 files changed, 16 insertions(+), 2 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 90cc3880cb84..e4eec7a919d9 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 @@ -213,8 +213,16 @@ public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInsta return null; } - return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, - this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + try { + return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, + this, aspectInstanceFactory, declarationOrderInAspect, aspectName); + } + catch (IllegalArgumentException | IllegalStateException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring incompatible advice method: " + candidateAdviceMethod, ex); + } + return null; + } } @Nullable diff --git a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml index 63d6e15591d4..bc5e5258f825 100644 --- a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml +++ b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml @@ -2,15 +2,21 @@ + + + + From 8d1bf9607bca236cdfbe87182145a60a9689a3b8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 23 May 2024 17:07:55 +0200 Subject: [PATCH 208/261] Polishing (cherry picked from commit 6c08d9399239f421d0a19fc838e3d9d623d7e2e0) --- .../cache/annotation/EnableCachingIntegrationTests.java | 2 +- .../core/env/EnvironmentSystemIntegrationTests.java | 3 +-- .../EnableTransactionManagementIntegrationTests.java | 5 ++--- .../cache/config/annotation-jcache-aspectj.xml | 3 +-- .../scheduling/aspectj/annotationDrivenContext.xml | 6 ++---- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java index 41acb6ed5d62..0660b9d0208e 100644 --- a/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/cache/annotation/EnableCachingIntegrationTests.java @@ -63,7 +63,7 @@ void repositoryUsesAspectJAdviceMode() { // attempt was made to look up the AJ aspect. It's due to classpath issues // in integration-tests that it's not found. assertThatException().isThrownBy(ctx::refresh) - .withMessageContaining("AspectJCachingConfiguration"); + .withMessageContaining("AspectJCachingConfiguration"); } diff --git a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java index 2bbc001f28eb..dc496f988b3a 100644 --- a/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/core/env/EnvironmentSystemIntegrationTests.java @@ -542,8 +542,7 @@ void abstractApplicationContextValidatesRequiredPropertiesOnRefresh() { { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.getEnvironment().setRequiredProperties("foo", "bar"); - assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy( - ctx::refresh); + assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(ctx::refresh); } { diff --git a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java index 4ab8cab1569e..ab3349863f3a 100644 --- a/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java +++ b/integration-tests/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementIntegrationTests.java @@ -98,9 +98,8 @@ void repositoryUsesAspectJAdviceMode() { // this test is a bit fragile, but gets the job done, proving that an // attempt was made to look up the AJ aspect. It's due to classpath issues // in integration-tests that it's not found. - assertThatException() - .isThrownBy(ctx::refresh) - .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); + assertThatException().isThrownBy(ctx::refresh) + .withMessageContaining("AspectJJtaTransactionManagementConfiguration"); } @Test diff --git a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml index 54ddbfd44a7b..9366abc646ee 100644 --- a/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml +++ b/spring-aspects/src/test/resources/org/springframework/cache/config/annotation-jcache-aspectj.xml @@ -24,8 +24,7 @@ - + diff --git a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml index 61d1d3a8e9bf..2bc3dc1d113d 100644 --- a/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml +++ b/spring-aspects/src/test/resources/org/springframework/scheduling/aspectj/annotationDrivenContext.xml @@ -7,12 +7,10 @@ http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd"> - + - + From 206a89017c1a1de3ad038f4a2621ad93cbca1c07 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 24 May 2024 12:29:31 +0200 Subject: [PATCH 209/261] Test detection of original generic method for CGLIB bridge method See gh-32888 --- .../springframework/beans/BeanUtilsTests.java | 98 ++++++++++++++++--- .../core/BridgeMethodResolver.java | 20 ++-- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java index 0d75f7c37187..3e374cc29040 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -41,6 +41,8 @@ import org.springframework.beans.testfixture.beans.DerivedTestBean; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; import org.springframework.lang.Nullable; @@ -51,7 +53,7 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; /** - * Unit tests for {@link BeanUtils}. + * Tests for {@link BeanUtils}. * * @author Juergen Hoeller * @author Rob Harrop @@ -321,12 +323,13 @@ void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() thro Order original = new Order("test", List.of("foo", "bar")); // Create a Proxy that loses the generic type information for the getLineItems() method. - OrderSummary proxy = proxyOrder(original); + OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {OrderSummary.class}, new OrderInvocationHandler(original)); assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString()) - .contains("java.util.List"); + .contains("java.util.List"); assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString()) - .contains("java.util.List") - .doesNotContain(""); + .contains("java.util.List") + .doesNotContain(""); // Ensure that our custom Proxy works as expected. assertThat(proxy.getId()).isEqualTo("test"); @@ -339,6 +342,23 @@ void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() thro assertThat(target.getLineItems()).containsExactly("foo", "bar"); } + @Test // gh-32888 + public void copyPropertiesWithGenericCglibClass() { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(User.class); + enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args)); + User user = (User) enhancer.create(); + user.setId(1); + user.setName("proxy"); + user.setAddress("addr"); + + User target = new User(); + BeanUtils.copyProperties(user, target); + assertThat(target.getId()).isEqualTo(user.getId()); + assertThat(target.getName()).isEqualTo(user.getName()); + assertThat(target.getAddress()).isEqualTo(user.getAddress()); + } + @Test void copyPropertiesWithEditable() throws Exception { TestBean tb = new TestBean(); @@ -518,6 +538,7 @@ public void setNumber(Number number) { } } + @SuppressWarnings("unused") private static class IntegerHolder { @@ -532,6 +553,7 @@ public void setNumber(Integer number) { } } + @SuppressWarnings("unused") private static class WildcardListHolder1 { @@ -546,6 +568,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class WildcardListHolder2 { @@ -560,6 +583,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class NumberUpperBoundedWildcardListHolder { @@ -574,6 +598,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class NumberListHolder { @@ -588,6 +613,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class IntegerListHolder1 { @@ -602,6 +628,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class IntegerListHolder2 { @@ -616,6 +643,7 @@ public void setList(List list) { } } + @SuppressWarnings("unused") private static class LongListHolder { @@ -796,6 +824,7 @@ public void setValue(String aValue) { } } + private static class BeanWithNullableTypes { private Integer counter; @@ -826,6 +855,7 @@ public String getValue() { } } + private static class BeanWithPrimitiveTypes { private boolean flag; @@ -838,7 +868,6 @@ private static class BeanWithPrimitiveTypes { private char character; private String text; - @SuppressWarnings("unused") public BeanWithPrimitiveTypes(boolean flag, byte byteCount, short shortCount, int intCount, long longCount, float floatCount, double doubleCount, char character, String text) { @@ -889,21 +918,22 @@ public char getCharacter() { public String getText() { return text; } - } + private static class PrivateBeanWithPrivateConstructor { private PrivateBeanWithPrivateConstructor() { } } + @SuppressWarnings("unused") private static class Order { private String id; - private List lineItems; + private List lineItems; Order() { } @@ -935,6 +965,7 @@ public String toString() { } } + private interface OrderSummary { String getId(); @@ -943,17 +974,10 @@ private interface OrderSummary { } - private OrderSummary proxyOrder(Order order) { - return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(), - new Class[] { OrderSummary.class }, new OrderInvocationHandler(order)); - } - - private static class OrderInvocationHandler implements InvocationHandler { private final Order order; - OrderInvocationHandler(Order order) { this.order = order; } @@ -971,4 +995,46 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } + + private static class GenericBaseModel { + + private T id; + + private String name; + + public T getId() { + return id; + } + + public void setId(T id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + + private static class User extends GenericBaseModel { + + private String address; + + public User() { + super(); + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + } + } diff --git a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java index 8aa4380f874a..811816478dd6 100644 --- a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java +++ b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -57,11 +57,11 @@ private BridgeMethodResolver() { /** - * Find the original method for the supplied {@link Method bridge Method}. + * Find the local original method for the supplied {@link Method bridge Method}. *

      It is safe to call this method passing in a non-bridge {@link Method} instance. * In such a case, the supplied {@link Method} instance is returned directly to the caller. * Callers are not required to check for bridging before calling this method. - * @param bridgeMethod the method to introspect + * @param bridgeMethod the method to introspect against its declaring class * @return the original method (either the bridged method or the passed-in method * if no more specific one could be found) */ @@ -73,8 +73,7 @@ public static Method findBridgedMethod(Method bridgeMethod) { if (bridgedMethod == null) { // Gather all methods with matching name and parameter size. List candidateMethods = new ArrayList<>(); - MethodFilter filter = candidateMethod -> - isBridgedCandidateFor(candidateMethod, bridgeMethod); + MethodFilter filter = (candidateMethod -> isBridgedCandidateFor(candidateMethod, bridgeMethod)); ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass(), candidateMethods::add, filter); if (!candidateMethods.isEmpty()) { bridgedMethod = candidateMethods.size() == 1 ? @@ -121,8 +120,8 @@ private static Method searchCandidates(List candidateMethods, Method bri return candidateMethod; } else if (previousMethod != null) { - sameSig = sameSig && - Arrays.equals(candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); + sameSig = sameSig && Arrays.equals( + candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes()); } previousMethod = candidateMethod; } @@ -163,7 +162,8 @@ private static boolean isResolvedTypeMatch(Method genericMethod, Method candidat } } // A non-array type: compare the type itself. - if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) { + if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals( + ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) { return false; } } @@ -226,8 +226,8 @@ private static Method searchForMatch(Class type, Method bridgeMethod) { /** * Compare the signatures of the bridge method and the method which it bridges. If * the parameter and return types are the same, it is a 'visibility' bridge method - * introduced in Java 6 to fix https://bugs.openjdk.org/browse/JDK-6342411. - * See also https://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html + * introduced in Java 6 to fix + * JDK-6342411. * @return whether signatures match as described */ public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) { From 079d53c8d69c17e58422303a9f6a80b952c27106 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:34:18 +0300 Subject: [PATCH 210/261] Support compilation of array and list indexing with Integer in SpEL Prior to this commit, the Spring Expression Language (SpEL) failed to compile an expression that indexed into an array or list using an Integer. This commit adds support for compilation of such expressions by ensuring that an Integer is unboxed into an int in the compiled bytecode. See gh-32694 Closes gh-32908 (cherry picked from commit cda577d1aacf0f90225f13050da42506f95bd2c8) --- .../expression/spel/ast/Indexer.java | 23 +- .../spel/SpelCompilationCoverageTests.java | 443 +++++++++++++++++- 2 files changed, 455 insertions(+), 11 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 7e80a576c13b..eabf26682ead 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +225,8 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { cf.loadTarget(mv); } + SpelNodeImpl index = this.children[0]; + if (this.indexedType == IndexedType.ARRAY) { int insn; if ("D".equals(this.exitTypeDescriptor)) { @@ -261,18 +263,14 @@ else if ("C".equals(this.exitTypeDescriptor)) { //depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;"); insn = AALOAD; } - SpelNodeImpl index = this.children[0]; - cf.enterCompilationScope(); - index.generateCode(mv, cf); - cf.exitCompilationScope(); + + generateIndexCode(mv, cf, index, int.class); mv.visitInsn(insn); } else if (this.indexedType == IndexedType.LIST) { mv.visitTypeInsn(CHECKCAST, "java/util/List"); - cf.enterCompilationScope(); - this.children[0].generateCode(mv, cf); - cf.exitCompilationScope(); + generateIndexCode(mv, cf, index, int.class); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true); } @@ -280,13 +278,13 @@ else if (this.indexedType == IndexedType.MAP) { mv.visitTypeInsn(CHECKCAST, "java/util/Map"); // Special case when the key is an unquoted string literal that will be parsed as // a property/field reference - if ((this.children[0] instanceof PropertyOrFieldReference reference)) { + if (index instanceof PropertyOrFieldReference reference) { String mapKeyName = reference.getName(); mv.visitLdcInsn(mapKeyName); } else { cf.enterCompilationScope(); - this.children[0].generateCode(mv, cf); + index.generateCode(mv, cf); cf.exitCompilationScope(); } mv.visitMethodInsn( @@ -323,6 +321,11 @@ else if (this.indexedType == IndexedType.OBJECT) { cf.pushDescriptor(this.exitTypeDescriptor); } + private void generateIndexCode(MethodVisitor mv, CodeFlow cf, SpelNodeImpl indexNode, Class indexType) { + String indexDesc = CodeFlow.toDescriptor(indexType); + generateCodeForArgument(mv, cf, indexNode, indexDesc); + } + @Override public String toStringAST() { StringJoiner sj = new StringJoiner(",", "[", "]"); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index c7fa4f887ee3..2c9772d288a1 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +20,8 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -27,7 +29,10 @@ import java.util.Map; import java.util.Set; import java.util.StringTokenizer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -132,6 +137,442 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { private SpelNodeImpl ast; + @Nested + class VariableReferenceTests { + + @Test + void userDefinedVariable() { + EvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("target", "abc"); + expression = parser.parseExpression("#target"); + assertThat(expression.getValue(ctx)).isEqualTo("abc"); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo("abc"); + ctx.setVariable("target", "123"); + assertThat(expression.getValue(ctx)).isEqualTo("123"); + + // Changing the variable type from String to Integer results in a + // ClassCastException in the compiled code. + ctx.setVariable("target", 42); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expression.getValue(ctx)) + .withCauseInstanceOf(ClassCastException.class); + + ctx.setVariable("target", "abc"); + expression = parser.parseExpression("#target.charAt(0)"); + assertThat(expression.getValue(ctx)).isEqualTo('a'); + assertCanCompile(expression); + assertThat(expression.getValue(ctx)).isEqualTo('a'); + ctx.setVariable("target", "1"); + assertThat(expression.getValue(ctx)).isEqualTo('1'); + + // Changing the variable type from String to Integer results in a + // ClassCastException in the compiled code. + ctx.setVariable("target", 42); + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expression.getValue(ctx)) + .withCauseInstanceOf(ClassCastException.class); + } + + } + + @Nested + class IndexingTests { + + @Test + void indexIntoPrimitiveShortArray() { + short[] shorts = { (short) 33, (short) 44, (short) 55 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(shorts)).isEqualTo((short) 55); + assertCanCompile(expression); + assertThat(expression.getValue(shorts)).isEqualTo((short) 55); + assertThat(getAst().getExitDescriptor()).isEqualTo("S"); + } + + @Test + void indexIntoPrimitiveByteArray() { + byte[] bytes = { (byte) 2, (byte) 3, (byte) 4 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(bytes)).isEqualTo((byte) 4); + assertCanCompile(expression); + assertThat(expression.getValue(bytes)).isEqualTo((byte) 4); + assertThat(getAst().getExitDescriptor()).isEqualTo("B"); + } + + @Test + void indexIntoPrimitiveIntArray() { + int[] ints = { 8, 9, 10 }; + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(ints)).isEqualTo(10); + assertCanCompile(expression); + assertThat(expression.getValue(ints)).isEqualTo(10); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test + void indexIntoPrimitiveLongArray() { + long[] longs = { 2L, 3L, 4L }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(longs)).isEqualTo(2L); + assertCanCompile(expression); + assertThat(expression.getValue(longs)).isEqualTo(2L); + assertThat(getAst().getExitDescriptor()).isEqualTo("J"); + } + + @Test + void indexIntoPrimitiveFloatArray() { + float[] floats = { 6.0f, 7.0f, 8.0f }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(floats)).isEqualTo(6.0f); + assertCanCompile(expression); + assertThat(expression.getValue(floats)).isEqualTo(6.0f); + assertThat(getAst().getExitDescriptor()).isEqualTo("F"); + } + + @Test + void indexIntoPrimitiveDoubleArray() { + double[] doubles = { 3.0d, 4.0d, 5.0d }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(doubles)).isEqualTo(4.0d); + assertCanCompile(expression); + assertThat(expression.getValue(doubles)).isEqualTo(4.0d); + assertThat(getAst().getExitDescriptor()).isEqualTo("D"); + } + + @Test + void indexIntoPrimitiveCharArray() { + char[] chars = { 'a', 'b', 'c' }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(chars)).isEqualTo('b'); + assertCanCompile(expression); + assertThat(expression.getValue(chars)).isEqualTo('b'); + assertThat(getAst().getExitDescriptor()).isEqualTo("C"); + } + + @Test + void indexIntoStringArray() { + String[] strings = { "a", "b", "c" }; + + expression = parser.parseExpression("[0]"); + + assertThat(expression.getValue(strings)).isEqualTo("a"); + assertCanCompile(expression); + assertThat(expression.getValue(strings)).isEqualTo("a"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + void indexIntoNumberArray() { + Number[] numbers = { 2, 8, 9 }; + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(numbers)).isEqualTo(8); + assertCanCompile(expression); + assertThat(expression.getValue(numbers)).isEqualTo(8); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number"); + } + + @Test + void indexInto2DPrimitiveIntArray() { + int[][] array = new int[][] { + { 1, 2, 3 }, + { 4, 5, 6 } + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6"); + assertThat(getAst().getExitDescriptor()).isEqualTo("[I"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("6"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("6"); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test + void indexInto2DStringArray() { + String[][] array = new String[][] { + { "a", "b", "c" }, + { "d", "e", "f" } + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + @SuppressWarnings("unchecked") + void indexIntoArrayOfListOfString() { + List[] array = new List[] { + List.of("a", "b", "c"), + List.of("d", "e", "f") + }; + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/List"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + @SuppressWarnings("unchecked") + void indexIntoArrayOfMap() { + Map[] array = new Map[] { Map.of("key", "value1") }; + + expression = parser.parseExpression("[0]"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); + assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map"); + + expression = parser.parseExpression("[0]['key']"); + + assertThat(stringify(expression.getValue(array))).isEqualTo("value1"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(array))).isEqualTo("value1"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfString() { + List list = List.of("aaa", "bbb", "ccc"); + + expression = parser.parseExpression("[1]"); + + assertThat(expression.getValue(list)).isEqualTo("bbb"); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo("bbb"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfInteger() { + List list = List.of(123, 456, 789); + + expression = parser.parseExpression("[2]"); + + assertThat(expression.getValue(list)).isEqualTo(789); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo(789); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoListOfStringArray() { + List list = List.of( + new String[] { "a", "b", "c" }, + new String[] { "d", "e", "f" } + ); + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[1][0]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("d"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test + void indexIntoListOfIntegerArray() { + List list = List.of( + new Integer[] { 1, 2, 3 }, + new Integer[] { 4, 5, 6 } + ); + + expression = parser.parseExpression("[0]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[0][1]"); + + assertThat(expression.getValue(list)).isEqualTo(2); + assertCanCompile(expression); + assertThat(expression.getValue(list)).isEqualTo(2); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer"); + } + + @Test + void indexIntoListOfListOfString() { + List> list = List.of( + List.of("a", "b", "c"), + List.of("d", "e", "f") + ); + + expression = parser.parseExpression("[1]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(list))).isEqualTo("d e f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("[1][2]"); + + assertThat(stringify(expression.getValue(list))).isEqualTo("f"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(list))).isEqualTo("f"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoMap() { + Map map = Map.of("aaa", 111); + + expression = parser.parseExpression("['aaa']"); + + assertThat(expression.getValue(map)).isEqualTo(111); + assertCanCompile(expression); + assertThat(expression.getValue(map)).isEqualTo(111); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoMapOfListOfString() { + Map> map = Map.of("foo", List.of("a", "b", "c")); + + expression = parser.parseExpression("['foo']"); + + assertThat(stringify(expression.getValue(map))).isEqualTo("a b c"); + assertCanCompile(expression); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + assertThat(stringify(expression.getValue(map))).isEqualTo("a b c"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + expression = parser.parseExpression("['foo'][2]"); + + assertThat(stringify(expression.getValue(map))).isEqualTo("c"); + assertCanCompile(expression); + assertThat(stringify(expression.getValue(map))).isEqualTo("c"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + @Test + void indexIntoObject() { + TestClass6 tc = new TestClass6(); + + // field access + expression = parser.parseExpression("['orange']"); + + assertThat(expression.getValue(tc)).isEqualTo("value1"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value1"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + + // field access + expression = parser.parseExpression("['peach']"); + + assertThat(expression.getValue(tc)).isEqualTo(34L); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo(34L); + assertThat(getAst().getExitDescriptor()).isEqualTo("J"); + + // property access (getter) + expression = parser.parseExpression("['banana']"); + + assertThat(expression.getValue(tc)).isEqualTo("value3"); + assertCanCompile(expression); + assertThat(expression.getValue(tc)).isEqualTo("value3"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String"); + } + + @Test // gh-32694, gh-32908 + void indexIntoArrayUsingIntegerWrapper() { + context.setVariable("array", new int[] {1, 2, 3, 4}); + context.setVariable("index", 2); + + expression = parser.parseExpression("#array[#index]"); + + assertThat(expression.getValue(context)).isEqualTo(3); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(3); + assertThat(getAst().getExitDescriptor()).isEqualTo("I"); + } + + @Test // gh-32694, gh-32908 + void indexIntoListUsingIntegerWrapper() { + context.setVariable("list", List.of(1, 2, 3, 4)); + context.setVariable("index", 2); + + expression = parser.parseExpression("#list[#index]"); + + assertThat(expression.getValue(context)).isEqualTo(3); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo(3); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + + private String stringify(Object object) { + Stream stream; + if (object instanceof Collection collection) { + stream = collection.stream(); + } + else if (object instanceof Object[] objects) { + stream = Arrays.stream(objects); + } + else if (object instanceof int[] ints) { + stream = Arrays.stream(ints).mapToObj(Integer::valueOf); + } + else { + return String.valueOf(object); + } + return stream.map(Object::toString).collect(Collectors.joining(" ")); + } + + } + @Test void typeReference() { expression = parse("T(String)"); From aed1d5f76271dd8b4082d77a1b42e258d1ad8c86 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 28 May 2024 10:19:15 +0200 Subject: [PATCH 211/261] Support compilation of map indexing with primitive in SpEL Prior to this commit, the Spring Expression Language (SpEL) failed to compile an expression that indexed into a Map using a primitive literal (boolean, int, long, float, or double). This commit adds support for compilation of such expressions by ensuring that primitive literals are boxed into their corresponding wrapper types in the compiled bytecode. Closes gh-32903 (cherry picked from commit e9de426eb5ca14b354073ec35f10fa0cdfeb1791) --- .../expression/spel/ast/Indexer.java | 4 +- .../spel/SpelCompilationCoverageTests.java | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index eabf26682ead..9dc12f91fdd0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -283,9 +283,7 @@ else if (this.indexedType == IndexedType.MAP) { mv.visitLdcInsn(mapKeyName); } else { - cf.enterCompilationScope(); - index.generateCode(mv, cf); - cf.exitCompilationScope(); + generateIndexCode(mv, cf, index, Object.class); } mv.visitMethodInsn( INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 2c9772d288a1..6667cc0cc0ce 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -554,6 +554,53 @@ void indexIntoListUsingIntegerWrapper() { assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); } + @Test // gh-32903 + void indexIntoMapUsingPrimitiveLiteral() { + Map map = Map.of( + false, "0", // BooleanLiteral + 1, "ABC", // IntLiteral + 2L, "XYZ", // LongLiteral + 9.99F, "~10", // FloatLiteral + 3.14159, "PI" // RealLiteral + ); + context.setVariable("map", map); + + // BooleanLiteral + expression = parser.parseExpression("#map[false]"); + assertThat(expression.getValue(context)).isEqualTo("0"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("0"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // IntLiteral + expression = parser.parseExpression("#map[1]"); + assertThat(expression.getValue(context)).isEqualTo("ABC"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("ABC"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // LongLiteral + expression = parser.parseExpression("#map[2L]"); + assertThat(expression.getValue(context)).isEqualTo("XYZ"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("XYZ"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // FloatLiteral + expression = parser.parseExpression("#map[9.99F]"); + assertThat(expression.getValue(context)).isEqualTo("~10"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("~10"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + + // RealLiteral + expression = parser.parseExpression("#map[3.14159]"); + assertThat(expression.getValue(context)).isEqualTo("PI"); + assertCanCompile(expression); + assertThat(expression.getValue(context)).isEqualTo("PI"); + assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); + } + private String stringify(Object object) { Stream stream; if (object instanceof Collection collection) { From 8b589db0282515c24f685d0d6d0c2bdefc611a76 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 3 Jun 2024 12:45:11 +0200 Subject: [PATCH 212/261] Avoid NoSuchMethodException for annotation attribute checks Closes gh-32921 (cherry picked from commit b08883b65c19cbed6e7a93990194977f5af73e0a) --- .../core/annotation/AnnotationUtils.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index 565e5055b70a..c169e3a38280 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -1049,16 +1049,16 @@ public static Object getValue(@Nullable Annotation annotation, @Nullable String return null; } try { - Method method = annotation.annotationType().getDeclaredMethod(attributeName); - return invokeAnnotationMethod(method, annotation); - } - catch (NoSuchMethodException ex) { - return null; + for (Method method : annotation.annotationType().getDeclaredMethods()) { + if (method.getName().equals(attributeName) && method.getParameterCount() == 0) { + return invokeAnnotationMethod(method, annotation); + } + } } catch (Throwable ex) { handleValueRetrievalFailure(annotation, ex); - return null; } + return null; } /** From 628b0504ec64b8ac7412fc71097c490a0ca653ac Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 6 Jun 2024 20:42:07 +0200 Subject: [PATCH 213/261] Skip ajc-compiled aspects for ajc-compiled target classes Includes defensive ignoring of incompatible aspect types. Closes gh-32970 (cherry picked from commit 0ea96b48065fe2c1bbbe4b537849ded0ad821280) --- .../aspectj/AspectJExpressionPointcut.java | 22 +++++++- .../BeanFactoryAspectJAdvisorsBuilder.java | 50 ++++++++++++------- .../aspectj/autoproxy/ajcAutoproxyTests.xml | 6 ++- 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index 72dc0edda1f0..97a1a5db8ae8 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.ObjectInputStream; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; @@ -85,6 +86,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware { + private static final String AJC_MAGIC = "ajc$"; + private static final Set SUPPORTED_PRIMITIVES = Set.of( PointcutPrimitive.EXECUTION, PointcutPrimitive.ARGS, @@ -102,6 +105,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut @Nullable private Class pointcutDeclarationScope; + private boolean aspectCompiledByAjc; + private String[] pointcutParameterNames = new String[0]; private Class[] pointcutParameterTypes = new Class[0]; @@ -133,7 +138,7 @@ public AspectJExpressionPointcut() { * @param paramTypes the parameter types for the pointcut */ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, Class[] paramTypes) { - this.pointcutDeclarationScope = declarationScope; + setPointcutDeclarationScope(declarationScope); if (paramNames.length != paramTypes.length) { throw new IllegalStateException( "Number of pointcut parameter names must match number of pointcut parameter types"); @@ -148,6 +153,7 @@ public AspectJExpressionPointcut(Class declarationScope, String[] paramNames, */ public void setPointcutDeclarationScope(Class pointcutDeclarationScope) { this.pointcutDeclarationScope = pointcutDeclarationScope; + this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope); } /** @@ -273,6 +279,11 @@ public PointcutExpression getPointcutExpression() { @Override public boolean matches(Class targetClass) { if (this.pointcutParsingFailed) { + // Pointcut parsing failed before below -> avoid trying again. + return false; + } + if (this.aspectCompiledByAjc && compiledByAjc(targetClass)) { + // ajc-compiled aspect class for ajc-compiled target class -> already weaved. return false; } @@ -528,6 +539,15 @@ else if (shadowMatch.maybeMatches() && fallbackExpression != null) { return shadowMatch; } + private static boolean compiledByAjc(Class clazz) { + for (Field field : clazz.getDeclaredFields()) { + if (field.getName().startsWith(AJC_MAGIC)) { + return true; + } + } + return false; + } + @Override public boolean equals(@Nullable Object other) { diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java index 8896f990ecbb..a318ea56bb49 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -22,9 +22,12 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.aspectj.lang.reflect.PerClauseKind; import org.springframework.aop.Advisor; +import org.springframework.aop.framework.AopConfigException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.lang.Nullable; @@ -40,6 +43,8 @@ */ public class BeanFactoryAspectJAdvisorsBuilder { + private static final Log logger = LogFactory.getLog(BeanFactoryAspectJAdvisorsBuilder.class); + private final ListableBeanFactory beanFactory; private final AspectJAdvisorFactory advisorFactory; @@ -102,30 +107,37 @@ public List buildAspectJAdvisors() { continue; } if (this.advisorFactory.isAspect(beanType)) { - aspectNames.add(beanName); - AspectMetadata amd = new AspectMetadata(beanType, beanName); - if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { - MetadataAwareAspectInstanceFactory factory = - new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); - List classAdvisors = this.advisorFactory.getAdvisors(factory); - if (this.beanFactory.isSingleton(beanName)) { - this.advisorsCache.put(beanName, classAdvisors); + try { + AspectMetadata amd = new AspectMetadata(beanType, beanName); + if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + MetadataAwareAspectInstanceFactory factory = + new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); + List classAdvisors = this.advisorFactory.getAdvisors(factory); + if (this.beanFactory.isSingleton(beanName)) { + this.advisorsCache.put(beanName, classAdvisors); + } + else { + this.aspectFactoryCache.put(beanName, factory); + } + advisors.addAll(classAdvisors); } else { + // Per target or per this. + if (this.beanFactory.isSingleton(beanName)) { + throw new IllegalArgumentException("Bean with name '" + beanName + + "' is a singleton, but aspect instantiation model is not singleton"); + } + MetadataAwareAspectInstanceFactory factory = + new PrototypeAspectInstanceFactory(this.beanFactory, beanName); this.aspectFactoryCache.put(beanName, factory); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); } - advisors.addAll(classAdvisors); + aspectNames.add(beanName); } - else { - // Per target or per this. - if (this.beanFactory.isSingleton(beanName)) { - throw new IllegalArgumentException("Bean with name '" + beanName + - "' is a singleton, but aspect instantiation model is not singleton"); + catch (IllegalArgumentException | IllegalStateException | AopConfigException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring incompatible aspect [" + beanType.getName() + "]: " + ex); } - MetadataAwareAspectInstanceFactory factory = - new PrototypeAspectInstanceFactory(this.beanFactory, beanName); - this.aspectFactoryCache.put(beanName, factory); - advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } } diff --git a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml index bc5e5258f825..e6c494c4f966 100644 --- a/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml +++ b/spring-aspects/src/test/resources/org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml @@ -21,8 +21,10 @@ - + - + + + From 1e2c4635a3bee59029ecc8378ff76b8b1b4fca02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 7 Jun 2024 11:49:43 +0200 Subject: [PATCH 214/261] Exclude node_modules from NoHttp checks Closes gh-32981 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a9ce6eceb7af..0e5defc1ee75 100644 --- a/build.gradle +++ b/build.gradle @@ -151,7 +151,7 @@ configure(rootProject) { def rootPath = file(rootDir).toPath() def projectDirs = allprojects.collect { it.projectDir } + "${rootDir}/buildSrc" projectDirs.forEach { dir -> - [ 'bin', 'build', 'out', '.settings' ] + [ 'bin', 'build', 'out', '.settings', 'node_modules' ] .collect { rootPath.relativize(new File(dir, it).toPath()) } .forEach { source.exclude "$it/**" } [ '.classpath', '.project' ] From df0764db5d941978fdbb6f7d81bebe6427af995d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 7 Jun 2024 13:38:13 +0200 Subject: [PATCH 215/261] Remove outdated copyright from index.adoc Closes gh-32984 --- framework-docs/modules/ROOT/pages/index.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/index.adoc b/framework-docs/modules/ROOT/pages/index.adoc index 29df7928806c..fd4d0f9b11cc 100644 --- a/framework-docs/modules/ROOT/pages/index.adoc +++ b/framework-docs/modules/ROOT/pages/index.adoc @@ -29,8 +29,6 @@ Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clem Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch -Copyright © 2002 - 2023 VMware, Inc. All Rights Reserved. - Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. From 38b7209dc5c7c78139833ac2aebdd5a1cb9362dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 7 Jun 2024 13:44:30 +0200 Subject: [PATCH 216/261] Polishing See gh-32984 --- framework-docs/modules/ROOT/pages/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/index.adoc b/framework-docs/modules/ROOT/pages/index.adoc index fd4d0f9b11cc..8b984f299a2d 100644 --- a/framework-docs/modules/ROOT/pages/index.adoc +++ b/framework-docs/modules/ROOT/pages/index.adoc @@ -31,4 +31,4 @@ Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each -copy contains this Copyright Notice, whether distributed in print or electronically. +copy contains the Copyright Notice, whether distributed in print or electronically. From 76604db8da1f4483afed7897be56a9ce54c17b44 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 7 Jun 2024 18:46:59 +0200 Subject: [PATCH 217/261] Stop observations for async requests in Servlet filter Prior to this commit, the `ServerHttpObservationFilter` would support async dispatches and would do the following: 1. start the observation 2. call the filter chain 3. if async has started, do nothing 4. if not in async mode, stop the observation This behavior would effectively rely on Async implementations to complete and dispatch the request back to the container for an async dispatch. This is what Spring web frameworks do and guarantee. Some implementations complete the async request but do not dispatch back; as a result, observations could leak as they are never stopped. This commit changes the support of async requests. The filter now opts-out of async dispatches - the filter will not be called for those anymore. Instead, if the application started async mode during the initial container dispatch, the filter will register an AsyncListener to be notified of the outcome of the async handling. Fixes gh-32986 --- .../filter/ServerHttpObservationFilter.java | 53 +++++++++++++++---- .../ServerHttpObservationFilterTests.java | 17 +++++- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java index 3ca50f609b59..d2b8bfcac364 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,9 +21,12 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; import jakarta.servlet.FilterChain; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -94,11 +97,6 @@ public static Optional findObservationContext(H return Optional.ofNullable((ServerRequestObservationContext) request.getAttribute(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE)); } - @Override - protected boolean shouldNotFilterAsyncDispatch() { - return false; - } - @Override @SuppressWarnings("try") protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -114,8 +112,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse throw ex; } finally { - // Only stop Observation if async processing is done or has never been started. - if (!request.isAsyncStarted()) { + // If async is started, register a listener for completion notification. + if (request.isAsyncStarted()) { + request.getAsyncContext().addListener(new ObservationAsyncListener(observation)); + } + // Stop Observation right now if async processing has not been started. + else { Throwable error = fetchException(request); if (error != null) { observation.error(error); @@ -139,13 +141,44 @@ private Observation createOrFetchObservation(HttpServletRequest request, HttpSer return observation; } - private Throwable unwrapServletException(Throwable ex) { + @Nullable + static Throwable unwrapServletException(Throwable ex) { return (ex instanceof ServletException) ? ex.getCause() : ex; } @Nullable - private Throwable fetchException(HttpServletRequest request) { + static Throwable fetchException(ServletRequest request) { return (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); } + private static class ObservationAsyncListener implements AsyncListener { + + private final Observation currentObservation; + + public ObservationAsyncListener(Observation currentObservation) { + this.currentObservation = currentObservation; + } + + @Override + public void onStartAsync(AsyncEvent event) { + } + + @Override + public void onTimeout(AsyncEvent event) { + this.currentObservation.stop(); + } + + @Override + public void onComplete(AsyncEvent event) { + this.currentObservation.stop(); + } + + @Override + public void onError(AsyncEvent event) { + this.currentObservation.error(unwrapServletException(event.getThrowable())); + this.currentObservation.stop(); + } + + } + } diff --git a/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java index 354e5c50a48f..80bdfbcbb274 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java @@ -50,6 +50,11 @@ class ServerHttpObservationFilterTests { private final MockHttpServletResponse response = new MockHttpServletResponse(); + @Test + void filterShouldNotProcessAsyncDispatch() { + assertThat(this.filter.shouldNotFilterAsyncDispatch()).isTrue(); + } + @Test void filterShouldFillObservationContext() throws Exception { this.filter.doFilter(this.request, this.response, this.mockFilterChain); @@ -60,7 +65,7 @@ void filterShouldFillObservationContext() throws Exception { assertThat(context.getCarrier()).isEqualTo(this.request); assertThat(context.getResponse()).isEqualTo(this.response); assertThat(context.getPathPattern()).isNull(); - assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS"); + assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped(); } @Test @@ -111,6 +116,16 @@ void filterShouldSetDefaultErrorStatusForBubblingExceptions() { .hasLowCardinalityKeyValue("status", "500"); } + @Test + void shouldCloseObservationAfterAsyncCompletion() throws Exception { + this.request.setAsyncSupported(true); + this.request.startAsync(); + this.filter.doFilter(this.request, this.response, this.mockFilterChain); + this.request.getAsyncContext().complete(); + + assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped(); + } + private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() { return TestObservationRegistryAssert.assertThat(this.observationRegistry) .hasObservationWithNameEqualTo("http.server.requests").that(); From eda868792a61e603c4fcd7fbeaa9bf910666e7f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 11 Jun 2024 14:35:56 +0200 Subject: [PATCH 218/261] Upgrade to Reactor 2022.0.20 Closes gh-33008 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index f7a2d290517f..623451741977 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.10.13")) api(platform("io.netty:netty-bom:4.1.109.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.19")) + api(platform("io.projectreactor:reactor-bom:2022.0.20")) api(platform("io.rsocket:rsocket-bom:1.1.3")) api(platform("org.apache.groovy:groovy-bom:4.0.21")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From 230848c8e5232fea3d944d6b0f3c440b553a80d4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Jun 2024 13:58:27 +0200 Subject: [PATCH 219/261] Upgrade to spring-javaformat-checkstyle 0.0.42 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0e5defc1ee75..299efbe9c806 100644 --- a/build.gradle +++ b/build.gradle @@ -106,7 +106,7 @@ configure([rootProject] + javaProjects) { project -> // JSR-305 only used for non-required meta-annotations compileOnly("com.google.code.findbugs:jsr305") testCompileOnly("com.google.code.findbugs:jsr305") - checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.41") + checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.42") } ext.javadocLinks = [ From df1f4812aabaa2ede40e24daeb38bf9ff22a5644 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Jun 2024 13:58:57 +0200 Subject: [PATCH 220/261] Upgrade to Netty 4.1.111 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 623451741977..3cc80ec967d4 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,7 +9,7 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.14.3")) api(platform("io.micrometer:micrometer-bom:1.10.13")) - api(platform("io.netty:netty-bom:4.1.109.Final")) + api(platform("io.netty:netty-bom:4.1.111.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2022.0.20")) api(platform("io.rsocket:rsocket-bom:1.1.3")) From b9eeee8341675c11eb0601bbf3c99c4c68423823 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Jun 2024 13:59:33 +0200 Subject: [PATCH 221/261] Polishing --- .../annotation-config/autowired-qualifiers.adoc | 15 ++++++++------- .../modules/ROOT/pages/core/beans/definition.adoc | 6 ++---- .../support/DefaultLifecycleProcessor.java | 10 ++++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc index 946fb6a317b2..9f064f052456 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired-qualifiers.adoc @@ -153,15 +153,16 @@ Letting qualifier values select against target bean names, within the type-match candidates, does not require a `@Qualifier` annotation at the injection point. If there is no other resolution indicator (such as a qualifier or a primary marker), for a non-unique dependency situation, Spring matches the injection point name -(that is, the field name or parameter name) against the target bean names and chooses the -same-named candidate, if any. +(that is, the field name or parameter name) against the target bean names and chooses +the same-named candidate, if any (either by bean name or by associated alias). + +This requires the `-parameters` Java compiler flag to be present. As a fallback, +version 6.0 still supports parameter names from debug symbols via `-debug` as well. ==== -That said, if you intend to express annotation-driven injection by name, do not -primarily use `@Autowired`, even if it is capable of selecting by bean name among -type-matching candidates. Instead, use the JSR-250 `@Resource` annotation, which is -semantically defined to identify a specific target component by its unique name, with -the declared type being irrelevant for the matching process. `@Autowired` has rather +As an alternative for injection by name, consider the JSR-250 `@Resource` annotation +which is semantically defined to identify a specific target component by its unique name, +with the declared type being irrelevant for the matching process. `@Autowired` has rather different semantics: After selecting candidate beans by type, the specified `String` qualifier value is considered within those type-selected candidates only (for example, matching an `account` qualifier against beans marked with the same qualifier label). diff --git a/framework-docs/modules/ROOT/pages/core/beans/definition.adoc b/framework-docs/modules/ROOT/pages/core/beans/definition.adoc index 2141abd706ff..dc7a68bb193c 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/definition.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/definition.adoc @@ -416,8 +416,8 @@ Kotlin:: ====== This approach shows that the factory bean itself can be managed and configured through -dependency injection (DI). See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail] -. +dependency injection (DI). +See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail]. NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the Spring container and that creates objects through an @@ -444,5 +444,3 @@ cases into account and returns the type of object that a `BeanFactory.getBean` c going to return for the same bean name. - - diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index 1cba2713651c..8467fe34480f 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -149,13 +149,13 @@ private void startBeans(boolean autoStartupOnly) { lifecycleBeans.forEach((beanName, bean) -> { if (!autoStartupOnly || (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())) { - int phase = getPhase(bean); - phases.computeIfAbsent( - phase, - p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) + int startupPhase = getPhase(bean); + phases.computeIfAbsent(startupPhase, + phase -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly) ).add(beanName, bean); } }); + if (!phases.isEmpty()) { phases.values().forEach(LifecycleGroup::start); } @@ -195,6 +195,7 @@ private void doStart(Map lifecycleBeans, String bea private void stopBeans() { Map lifecycleBeans = getLifecycleBeans(); Map phases = new HashMap<>(); + lifecycleBeans.forEach((beanName, bean) -> { int shutdownPhase = getPhase(bean); LifecycleGroup group = phases.get(shutdownPhase); @@ -204,6 +205,7 @@ private void stopBeans() { } group.add(beanName, bean); }); + if (!phases.isEmpty()) { List keys = new ArrayList<>(phases.keySet()); keys.sort(Collections.reverseOrder()); From f285df692cd16197046edd4b32cdb79ba29214d2 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 12 Jun 2024 14:42:49 +0200 Subject: [PATCH 222/261] Upgrade to SLF4J 2.0.13, JRuby 9.4.7, Awaitility 4.2.1 --- framework-platform/framework-platform.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 3cc80ec967d4..3808e4016d55 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -107,7 +107,7 @@ dependencies { api("org.aspectj:aspectjtools:1.9.22.1") api("org.aspectj:aspectjweaver:1.9.22.1") api("org.assertj:assertj-core:3.24.2") - api("org.awaitility:awaitility:4.2.0") + api("org.awaitility:awaitility:4.2.1") api("org.bouncycastle:bcpkix-jdk18on:1.72") api("org.codehaus.jettison:jettison:1.5.4") api("org.dom4j:dom4j:2.1.4") @@ -126,7 +126,7 @@ dependencies { api("org.hibernate:hibernate-validator:7.0.5.Final") api("org.hsqldb:hsqldb:2.7.2") api("org.javamoney:moneta:1.4.2") - api("org.jruby:jruby:9.4.6.0") + api("org.jruby:jruby:9.4.7.0") api("org.junit.support:testng-engine:1.0.4") api("org.mozilla:rhino:1.7.14") api("org.ogce:xpp3:1.1.6") @@ -135,7 +135,7 @@ dependencies { api("org.seleniumhq.selenium:htmlunit-driver:2.70.0") api("org.seleniumhq.selenium:selenium-java:3.141.59") api("org.skyscreamer:jsonassert:1.5.1") - api("org.slf4j:slf4j-api:2.0.12") + api("org.slf4j:slf4j-api:2.0.13") api("org.testng:testng:7.8.0") api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-core:0.55") From 2d83fca57188deb8e6adca3253362a8ecce85f35 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 13 Jun 2024 09:24:34 +0000 Subject: [PATCH 223/261] Next development version (v6.0.23-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a40d4261e3d0..07d3d7f9db9e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.0.22-SNAPSHOT +version=6.0.23-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 67686d776e3025ccc8401b804e2c074fa1d5d8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 13 Jun 2024 14:10:00 +0200 Subject: [PATCH 224/261] Fix invalid character in Javadoc of BeanFactory --- .../java/org/springframework/beans/factory/BeanFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java index 696a9c1cd59e..e61bd06599d6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java @@ -239,7 +239,7 @@ public interface BeanFactory { * specific type, specify the actual bean type as an argument here and subsequently * use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options. *

      Also, generics matching is strict here, as per the Java assignment rules. - * For lenient fallback matching with unchecked semantics (similar to the ´unchecked´ + * For lenient fallback matching with unchecked semantics (similar to the 'unchecked' * Java compiler warning), consider calling {@link #getBeanProvider(Class)} with the * raw type as a second step if no full generic match is * {@link ObjectProvider#getIfAvailable() available} with this variant. From 77964374f9823fa44d16f6c7dfbc26ab7031dd33 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 14 Jun 2024 22:07:46 +0200 Subject: [PATCH 225/261] Do not attempt to load pre-enhanced class for reloadable classes Closes gh-33024 (cherry picked from commit 089e4e69f1adbbe45d3a17807e7268a57e9a5ddf) --- .../ConfigurationClassEnhancer.java | 11 ++- .../ConfigurationClassEnhancerTests.java | 87 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java index 2521a0f2f5a6..db2f77a20ff4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java @@ -47,6 +47,7 @@ import org.springframework.cglib.proxy.NoOp; import org.springframework.cglib.transform.ClassEmitterTransformer; import org.springframework.cglib.transform.TransformingClassGenerator; +import org.springframework.core.SmartClassLoader; import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisException; import org.springframework.objenesis.SpringObjenesis; @@ -123,13 +124,21 @@ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader cl enhancer.setInterfaces(new Class[] {EnhancedConfiguration.class}); enhancer.setUseFactory(false); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(!isClassReloadable(configSuperClass, classLoader)); enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader)); enhancer.setCallbackFilter(CALLBACK_FILTER); enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes()); return enhancer; } + /** + * Checks whether the given configuration class is reloadable. + */ + private boolean isClassReloadable(Class configSuperClass, @Nullable ClassLoader classLoader) { + return (classLoader instanceof SmartClassLoader smartClassLoader && + smartClassLoader.isClassReloadable(configSuperClass)); + } + /** * Uses enhancer to generate a subclass of superclass, * ensuring that callbacks are registered for the new subclass. diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java new file mode 100644 index 000000000000..96051f161729 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2024 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.context.annotation; + +import java.io.IOException; +import java.io.InputStream; +import java.security.SecureClassLoader; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.SmartClassLoader; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Phillip Webb + * @author Juergen Hoeller + */ +class ConfigurationClassEnhancerTests { + + @Test + void enhanceReloadedClass() throws Exception { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + ClassLoader parentClassLoader = getClass().getClassLoader(); + CustomClassLoader classLoader = new CustomClassLoader(parentClassLoader); + Class myClass = parentClassLoader.loadClass(MyConfig.class.getName()); + configurationClassEnhancer.enhance(myClass, parentClassLoader); + Class myReloadedClass = classLoader.loadClass(MyConfig.class.getName()); + Class enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader); + assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader); + } + + + @Configuration + static class MyConfig { + + @Bean + public String myBean() { + return "bean"; + } + } + + + static class CustomClassLoader extends SecureClassLoader implements SmartClassLoader { + + CustomClassLoader(ClassLoader parent) { + super(parent); + } + + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.contains("MyConfig")) { + String path = name.replace('.', '/').concat(".class"); + try (InputStream in = super.getResourceAsStream(path)) { + byte[] bytes = StreamUtils.copyToByteArray(in); + if (bytes.length > 0) { + return defineClass(name, bytes, 0, bytes.length); + } + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + return super.loadClass(name, resolve); + } + + @Override + public boolean isClassReloadable(Class clazz) { + return clazz.getName().contains("MyConfig"); + } + } + +} From e17afed0fe764a14d835f041821726c5d0005a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 16 Jun 2024 16:47:36 +0200 Subject: [PATCH 226/261] Fix property name in Container Extension Points section Closes gh-33038 --- .../modules/ROOT/pages/core/beans/factory-extension.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc index a82606dc0e9b..0e517eb70511 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc @@ -439,7 +439,7 @@ dataSource.url=jdbc:mysql:mydb ---- This example file can be used with a container definition that contains a bean called -`dataSource` that has `driver` and `url` properties. +`dataSource` that has `driverClassName` and `url` properties. Compound property names are also supported, as long as every component of the path except the final property being overridden is already non-null (presumably initialized From f87b1bd6ce89d2b075cf32ce236c04dbd432f6ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 17 Jun 2024 10:06:47 +0000 Subject: [PATCH 227/261] Update Antora Spring UI to v0.4.16 --- framework-docs/antora-playbook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml index 9fc2d93216e3..5eda3bc121f5 100644 --- a/framework-docs/antora-playbook.yml +++ b/framework-docs/antora-playbook.yml @@ -36,4 +36,4 @@ runtime: failure_level: warn ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip From debf5717224f1208e3d723b0e401b179544bb0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 17 Jun 2024 13:39:18 +0200 Subject: [PATCH 228/261] Fix typo Closes gh-33054 --- .../modules/ROOT/pages/integration/jms/annotated.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc b/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc index edda9de3d387..8fc2f9cff305 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/annotated.adoc @@ -58,7 +58,7 @@ your `@Configuration` classes, as the following example shows: By default, the infrastructure looks for a bean named `jmsListenerContainerFactory` as the source for the factory to use to create message listener containers. In this case (and ignoring the JMS infrastructure setup), you can invoke the `processOrder` -method with a core poll size of three threads and a maximum pool size of ten threads. +method with a core pool size of three threads and a maximum pool size of ten threads. You can customize the listener container factory to use for each annotation or you can configure an explicit default by implementing the `JmsListenerConfigurer` interface. From 12949becfcaeee49f10fc2ca814b2f4ec0e1f15a Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 17 Jun 2024 18:42:30 +0200 Subject: [PATCH 229/261] Correct and consistent event class names in constructor javadoc Closes gh-33032 (cherry picked from commit e79a9a5bff15323545e21b812f7cea8585f8eddd) --- .../context/event/ApplicationContextEvent.java | 4 ++-- .../org/springframework/context/event/ContextClosedEvent.java | 4 ++-- .../springframework/context/event/ContextRefreshedEvent.java | 4 ++-- .../springframework/context/event/ContextStartedEvent.java | 4 ++-- .../springframework/context/event/ContextStoppedEvent.java | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java index fab9067b20d6..0823d051c340 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationContextEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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,7 +29,7 @@ public abstract class ApplicationContextEvent extends ApplicationEvent { /** - * Create a new ContextStartedEvent. + * Create a new {@code ApplicationContextEvent}. * @param source the {@code ApplicationContext} that the event is raised for * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java index 900bf30e49ca..8d0e2e56541c 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextClosedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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,7 +29,7 @@ public class ContextClosedEvent extends ApplicationContextEvent { /** - * Creates a new ContextClosedEvent. + * Create a new {@code ContextClosedEvent}. * @param source the {@code ApplicationContext} that has been closed * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java index 27c657a948e6..ba55c6a56c27 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextRefreshedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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,7 +29,7 @@ public class ContextRefreshedEvent extends ApplicationContextEvent { /** - * Create a new ContextRefreshedEvent. + * Create a new {@code ContextRefreshedEvent}. * @param source the {@code ApplicationContext} that has been initialized * or refreshed (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java index bfd615d5c120..f0cf6d6bb0d4 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStartedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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,7 +30,7 @@ public class ContextStartedEvent extends ApplicationContextEvent { /** - * Create a new ContextStartedEvent. + * Create a new {@code ContextStartedEvent}. * @param source the {@code ApplicationContext} that has been started * (must not be {@code null}) */ diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java index 4a156b207b8c..791e08c282c2 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextStoppedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2024 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,7 +30,7 @@ public class ContextStoppedEvent extends ApplicationContextEvent { /** - * Create a new ContextStoppedEvent. + * Create a new {@code ContextStoppedEvent}. * @param source the {@code ApplicationContext} that has been stopped * (must not be {@code null}) */ From db8d2d1626627152b3a62e16a838e5a92f4940b9 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 17 Jun 2024 19:06:49 +0200 Subject: [PATCH 230/261] Backported test for @Autowired @Bean method on configuration subclass See gh-33030 --- .../AutowiredConfigurationTests.java | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java index 9a9795476579..00cd00e3b8cf 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/AutowiredConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -43,6 +44,7 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -91,7 +93,7 @@ void testAutowiredConfigurationMethodDependenciesWithOptionalAndNotAvailable() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( OptionalAutowiredMethodConfig.class); - assertThat(context.getBeansOfType(Colour.class).isEmpty()).isTrue(); + assertThat(context.getBeansOfType(Colour.class)).isEmpty(); assertThat(context.getBean(TestBean.class).getName()).isEmpty(); context.close(); } @@ -183,14 +185,22 @@ void testValueInjectionWithProviderMethodArguments() { context.close(); } + @Test + void testValueInjectionWithAccidentalAutowiredAnnotations() { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(ValueConfigWithAccidentalAutowiredAnnotations.class); + doTestValueInjection(context); + context.close(); + } + private void doTestValueInjection(BeanFactory context) { System.clearProperty("myProp"); TestBean testBean = context.getBean("testBean", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); testBean = context.getBean("testBean2", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); System.setProperty("myProp", "foo"); @@ -203,10 +213,10 @@ private void doTestValueInjection(BeanFactory context) { System.clearProperty("myProp"); testBean = context.getBean("testBean", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); testBean = context.getBean("testBean2", TestBean.class); - assertThat((Object) testBean.getName()).isNull(); + assertThat(testBean.getName()).isNull(); } @Test @@ -281,7 +291,7 @@ public TestBean testBean(Optional colour, Optional> colours return new TestBean(""); } else { - return new TestBean(colour.get().toString() + "-" + colours.get().get(0).toString()); + return new TestBean(colour.get() + "-" + colours.get().get(0).toString()); } } } @@ -494,6 +504,32 @@ public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider } + @Configuration + static class ValueConfigWithAccidentalAutowiredAnnotations implements InitializingBean { + + boolean invoked; + + @Override + public void afterPropertiesSet() { + Assert.state(!invoked, "Factory method must not get invoked on startup"); + } + + @Bean @Scope("prototype") + @Autowired + public TestBean testBean(@Value("#{systemProperties[myProp]}") Provider name) { + invoked = true; + return new TestBean(name.get()); + } + + @Bean @Scope("prototype") + @Autowired + public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider name2) { + invoked = true; + return new TestBean(name2.get()); + } + } + + @Configuration static class PropertiesConfig { From 5a83fc8f8605cc191a8262ba87099c3afd651022 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 17 Jun 2024 19:07:04 +0200 Subject: [PATCH 231/261] Polishing --- .../annotation/AutowiredAnnotationBeanPostProcessor.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index b071f7c46b23..f849c1d8b0e7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -506,9 +506,9 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str } /** - * 'Native' processing method for direct calls with an arbitrary target instance, - * resolving all of its fields and methods which are annotated with one of the - * configured 'autowired' annotation types. + * Native processing method for direct calls with an arbitrary target + * instance, resolving all of its fields and methods which are annotated with + * one of the configured 'autowired' annotation types. * @param bean the target instance to process * @throws BeanCreationException if autowiring failed * @see #setAutowiredAnnotationTypes(Set) @@ -1090,7 +1090,6 @@ private void registerProxyIfNecessary(RuntimeHints runtimeHints, DependencyDescr } } } - } } From f59b56c4c93702a3d70cdd03b3d3f26f3c14677c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 18 Jun 2024 19:25:20 +0200 Subject: [PATCH 232/261] Use Sonatype S01 token in release pipeline --- ci/pipeline.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 770f9ccb1244..cb38e8950003 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -7,8 +7,8 @@ anchors: gradle-enterprise-task-params: &gradle-enterprise-task-params DEVELOCITY_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) sonatype-task-params: &sonatype-task-params - SONATYPE_USERNAME: ((sonatype-username)) - SONATYPE_PASSWORD: ((sonatype-password)) + SONATYPE_USERNAME: ((s01-user-token)) + SONATYPE_PASSWORD: ((s01-user-token-password)) SONATYPE_URL: ((sonatype-url)) SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile)) artifactory-task-params: &artifactory-task-params From 06fc5bc3eacafa7d45d83ccd1bde0547bd8f8db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 20 Jun 2024 14:15:54 +0200 Subject: [PATCH 233/261] Exclude GitHub Actions bot from changelog Closes gh-33077 --- ci/config/changelog-generator.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ci/config/changelog-generator.yml b/ci/config/changelog-generator.yml index 2252d20802e4..853cbb93994e 100644 --- a/ci/config/changelog-generator.yml +++ b/ci/config/changelog-generator.yml @@ -17,4 +17,13 @@ changelog: - "type: dependency-upgrade" contributors: exclude: - names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll", "simonbasle"] + names: + - "bclozel" + - "github-actions" + - "jhoeller" + - "poutsma" + - "rstoyanchev" + - "sbrannen" + - "sdeleuze" + - "simonbasle" + - "snicoll" From 3498a35377407a3bd5461642a9dbf313d825a941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 21 Jun 2024 14:14:05 +0200 Subject: [PATCH 234/261] Fix name of GitHub actions bot See gh-gh-33077 --- ci/config/changelog-generator.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/config/changelog-generator.yml b/ci/config/changelog-generator.yml index 853cbb93994e..082f16ed566a 100644 --- a/ci/config/changelog-generator.yml +++ b/ci/config/changelog-generator.yml @@ -19,7 +19,7 @@ changelog: exclude: names: - "bclozel" - - "github-actions" + - "github-actions[bot]" - "jhoeller" - "poutsma" - "rstoyanchev" From a352d884c986a9ed3492ce186d954fc31dad15d4 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 26 Jun 2024 11:45:25 +0200 Subject: [PATCH 235/261] Re-enable async dispatches in Observation Filter Prior to this commit, the fix for gh-32730 disabled the involvment of the osbervation filter for async dispatches. Instead of relying on ASYNC dispatches to close the observation for async requests, this is now using an async listener instead: async dispatches are not guaranteed to happen once the async request is handled. This change caused another side-effect: because async dispatches are not considered anymore by this filter, the observation scope is not reinstated for async dispatches. For example, `ResponseBodyAdvice` implementations do not have the observation scope opened during their execution. This commit re-enables async dispatches for this filter, but ensures that observations are not closed during such dispatches as this will be done by the async listener. Fixes gh-33128 --- .../filter/ServerHttpObservationFilter.java | 12 +++- .../ServerHttpObservationFilterTests.java | 72 +++++++++++++++++-- 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java index d2b8bfcac364..e56b297349d8 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ServerHttpObservationFilter.java @@ -23,6 +23,7 @@ import io.micrometer.observation.ObservationRegistry; import jakarta.servlet.AsyncEvent; import jakarta.servlet.AsyncListener; +import jakarta.servlet.DispatcherType; import jakarta.servlet.FilterChain; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; @@ -97,6 +98,11 @@ public static Optional findObservationContext(H return Optional.ofNullable((ServerRequestObservationContext) request.getAttribute(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE)); } + @Override + protected boolean shouldNotFilterAsyncDispatch() { + return false; + } + @Override @SuppressWarnings("try") protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -116,8 +122,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (request.isAsyncStarted()) { request.getAsyncContext().addListener(new ObservationAsyncListener(observation)); } - // Stop Observation right now if async processing has not been started. - else { + // scope is opened for ASYNC dispatches, but the observation will be closed + // by the async listener. + else if (request.getDispatcherType() != DispatcherType.ASYNC){ Throwable error = fetchException(request); if (error != null) { observation.error(error); @@ -176,7 +183,6 @@ public void onComplete(AsyncEvent event) { @Override public void onError(AsyncEvent event) { this.currentObservation.error(unwrapServletException(event.getThrowable())); - this.currentObservation.stop(); } } diff --git a/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java index 80bdfbcbb274..9a7eeb4f4111 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ServerHttpObservationFilterTests.java @@ -16,15 +16,24 @@ package org.springframework.web.filter; +import java.io.IOException; + import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; +import jakarta.servlet.AsyncEvent; +import jakarta.servlet.AsyncListener; +import jakarta.servlet.DispatcherType; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.http.server.observation.ServerRequestObservationContext; +import org.springframework.web.testfixture.servlet.MockAsyncContext; import org.springframework.web.testfixture.servlet.MockFilterChain; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; @@ -41,18 +50,18 @@ class ServerHttpObservationFilterTests { private final TestObservationRegistry observationRegistry = TestObservationRegistry.create(); - private final ServerHttpObservationFilter filter = new ServerHttpObservationFilter(this.observationRegistry); - - private final MockFilterChain mockFilterChain = new MockFilterChain(); - private final MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/resource/test"); private final MockHttpServletResponse response = new MockHttpServletResponse(); + private MockFilterChain mockFilterChain = new MockFilterChain(); + + private ServerHttpObservationFilter filter = new ServerHttpObservationFilter(this.observationRegistry); + @Test - void filterShouldNotProcessAsyncDispatch() { - assertThat(this.filter.shouldNotFilterAsyncDispatch()).isTrue(); + void filterShouldProcessAsyncDispatch() { + assertThat(this.filter.shouldNotFilterAsyncDispatch()).isFalse(); } @Test @@ -68,6 +77,12 @@ void filterShouldFillObservationContext() throws Exception { assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped(); } + @Test + void filterShouldOpenScope() throws Exception { + this.mockFilterChain = new MockFilterChain(new ScopeCheckingServlet(this.observationRegistry)); + filter.doFilter(this.request, this.response, this.mockFilterChain); + } + @Test void filterShouldAcceptNoOpObservationContext() throws Exception { ServerHttpObservationFilter filter = new ServerHttpObservationFilter(ObservationRegistry.NOOP); @@ -126,9 +141,52 @@ void shouldCloseObservationAfterAsyncCompletion() throws Exception { assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped(); } + @Test + void shouldCloseObservationAfterAsyncError() throws Exception { + this.request.setAsyncSupported(true); + this.request.startAsync(); + this.filter.doFilter(this.request, this.response, this.mockFilterChain); + MockAsyncContext asyncContext = (MockAsyncContext) this.request.getAsyncContext(); + for (AsyncListener listener : asyncContext.getListeners()) { + listener.onError(new AsyncEvent(this.request.getAsyncContext(), new IllegalStateException("test error"))); + } + asyncContext.complete(); + assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "IllegalStateException").hasBeenStopped(); + } + + @Test + void shouldNotCloseObservationDuringAsyncDispatch() throws Exception { + this.mockFilterChain = new MockFilterChain(new ScopeCheckingServlet(this.observationRegistry)); + this.request.setDispatcherType(DispatcherType.ASYNC); + this.filter.doFilter(this.request, this.response, this.mockFilterChain); + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .hasObservationWithNameEqualTo("http.server.requests") + .that().isNotStopped(); + } + private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() { + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .hasNumberOfObservationsWithNameEqualTo("http.server.requests", 1); + return TestObservationRegistryAssert.assertThat(this.observationRegistry) - .hasObservationWithNameEqualTo("http.server.requests").that(); + .hasObservationWithNameEqualTo("http.server.requests") + .that() + .hasBeenStopped(); + } + + @SuppressWarnings("serial") + static class ScopeCheckingServlet extends HttpServlet { + + private final ObservationRegistry observationRegistry; + + public ScopeCheckingServlet(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + assertThat(this.observationRegistry.getCurrentObservation()).isNotNull(); + } } } From 069d0819ddce0a39f08e688c97b1c31deb29d4d8 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 3 Jul 2024 16:36:18 +0200 Subject: [PATCH 236/261] Apply fallback resolution for non-hierarchical URIs such as "file:." Includes meaningful exception message for file system resolution. Closes gh-33124 (cherry picked from commit daea3f0eaed755814c6a32e9837e59cc95893923) --- .../beans/propertyeditors/PathEditor.java | 17 +++++--- .../propertyeditors/FileEditorTests.java | 40 ++++++++++++------- .../propertyeditors/PathEditorTests.java | 35 ++++++++++------ 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java index 70eb403d6a3d..70e348f09d2b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -92,9 +92,9 @@ public void setAsText(String text) throws IllegalArgumentException { // a file prefix (let's try as Spring resource location) nioPathCandidate = !text.startsWith(ResourceUtils.FILE_URL_PREFIX); } - catch (FileSystemNotFoundException ex) { - // URI scheme not registered for NIO (let's try URL - // protocol handlers via Spring's resource mechanism). + catch (FileSystemNotFoundException | IllegalArgumentException ex) { + // URI scheme not registered for NIO or not meeting Paths requirements: + // let's try URL protocol handlers via Spring's resource mechanism. } } @@ -111,8 +111,13 @@ else if (nioPathCandidate && !resource.exists()) { setValue(resource.getFile().toPath()); } catch (IOException ex) { - throw new IllegalArgumentException( - "Could not retrieve file for " + resource + ": " + ex.getMessage()); + String msg = "Could not resolve \"" + text + "\" to 'java.nio.file.Path' for " + resource + ": " + + ex.getMessage(); + if (nioPathCandidate) { + msg += " - In case of ambiguity, consider adding the 'file:' prefix for an explicit reference " + + "to a file system resource of the same name: \"file:" + text + "\""; + } + throw new IllegalArgumentException(msg); } } } diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java index 3076977e9ec4..84d6eff38126 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/FileEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -31,54 +31,64 @@ * @author Chris Beams * @author Juergen Hoeller */ -public class FileEditorTests { +class FileEditorTests { @Test - public void testClasspathFileName() { + void testClasspathFileName() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).exists(); } @Test - public void testWithNonExistentResource() { - PropertyEditor propertyEditor = new FileEditor(); + void testWithNonExistentResource() { + PropertyEditor fileEditor = new FileEditor(); assertThatIllegalArgumentException().isThrownBy(() -> - propertyEditor.setAsText("classpath:no_way_this_file_is_found.doc")); + fileEditor.setAsText("classpath:no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentFile() { + void testWithNonExistentFile() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("file:no_way_this_file_is_found.doc"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); } @Test - public void testAbsoluteFileName() { + void testAbsoluteFileName() { PropertyEditor fileEditor = new FileEditor(); fileEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); } @Test - public void testUnqualifiedFileNameFound() { + void testCurrentDirectory() { + PropertyEditor fileEditor = new FileEditor(); + fileEditor.setAsText("file:."); + Object value = fileEditor.getValue(); + assertThat(value).isInstanceOf(File.class); + File file = (File) value; + assertThat(file).isEqualTo(new File(".")); + } + + @Test + void testUnqualifiedFileNameFound() { PropertyEditor fileEditor = new FileEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; fileEditor.setAsText(fileName); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).exists(); String absolutePath = file.getAbsolutePath().replace('\\', '/'); @@ -86,13 +96,13 @@ public void testUnqualifiedFileNameFound() { } @Test - public void testUnqualifiedFileNameNotFound() { + void testUnqualifiedFileNameNotFound() { PropertyEditor fileEditor = new FileEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; fileEditor.setAsText(fileName); Object value = fileEditor.getValue(); - assertThat(value instanceof File).isTrue(); + assertThat(value).isInstanceOf(File.class); File file = (File) value; assertThat(file).doesNotExist(); String absolutePath = file.getAbsolutePath().replace('\\', '/'); diff --git a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java index d55cc18d48a4..ed4058b4fe53 100644 --- a/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/propertyeditors/PathEditorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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.beans.PropertyEditor; import java.io.File; import java.nio.file.Path; +import java.nio.file.Paths; import org.junit.jupiter.api.Test; @@ -31,10 +32,10 @@ * @author Juergen Hoeller * @since 4.3.2 */ -public class PathEditorTests { +class PathEditorTests { @Test - public void testClasspathPathName() { + void testClasspathPathName() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("classpath:" + ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"); @@ -45,14 +46,14 @@ public void testClasspathPathName() { } @Test - public void testWithNonExistentResource() { - PropertyEditor propertyEditor = new PathEditor(); + void testWithNonExistentResource() { + PropertyEditor pathEditor = new PathEditor(); assertThatIllegalArgumentException().isThrownBy(() -> - propertyEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); + pathEditor.setAsText("classpath:/no_way_this_file_is_found.doc")); } @Test - public void testWithNonExistentPath() { + void testWithNonExistentPath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("file:/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -62,7 +63,7 @@ public void testWithNonExistentPath() { } @Test - public void testAbsolutePath() { + void testAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("/no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -72,7 +73,7 @@ public void testAbsolutePath() { } @Test - public void testWindowsAbsolutePath() { + void testWindowsAbsolutePath() { PropertyEditor pathEditor = new PathEditor(); pathEditor.setAsText("C:\\no_way_this_file_is_found.doc"); Object value = pathEditor.getValue(); @@ -82,7 +83,7 @@ public void testWindowsAbsolutePath() { } @Test - public void testWindowsAbsoluteFilePath() { + void testWindowsAbsoluteFilePath() { PropertyEditor pathEditor = new PathEditor(); try { pathEditor.setAsText("file://C:\\no_way_this_file_is_found.doc"); @@ -99,7 +100,17 @@ public void testWindowsAbsoluteFilePath() { } @Test - public void testUnqualifiedPathNameFound() { + void testCurrentDirectory() { + PropertyEditor pathEditor = new PathEditor(); + pathEditor.setAsText("file:."); + Object value = pathEditor.getValue(); + assertThat(value).isInstanceOf(Path.class); + Path path = (Path) value; + assertThat(path).isEqualTo(Paths.get(".")); + } + + @Test + void testUnqualifiedPathNameFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".class"; @@ -117,7 +128,7 @@ public void testUnqualifiedPathNameFound() { } @Test - public void testUnqualifiedPathNameNotFound() { + void testUnqualifiedPathNameNotFound() { PropertyEditor pathEditor = new PathEditor(); String fileName = ClassUtils.classPackageAsResourcePath(getClass()) + "/" + ClassUtils.getShortName(getClass()) + ".clazz"; From 09123def57edf9a216bba111acac3d5ce21dd5f1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 28 Jun 2024 11:36:17 +0200 Subject: [PATCH 237/261] Detect ajc markers in superclasses as well (for weaving check) Closes gh-33113 (cherry picked from commit 100da83913884510103423d54257c37f2a428792) --- .../springframework/aop/aspectj/AspectJExpressionPointcut.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java index 97a1a5db8ae8..c83d6e1406cd 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcut.java @@ -545,7 +545,8 @@ private static boolean compiledByAjc(Class clazz) { return true; } } - return false; + Class superclass = clazz.getSuperclass(); + return (superclass != null && compiledByAjc(superclass)); } From c7fd5c9c34ff199392767ef4a64177eaeb73af9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 4 Jul 2024 10:23:15 +0200 Subject: [PATCH 238/261] Upgrade to Gradle 8.8 Closes gh-33147 --- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f05..a4413138c96c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426907..b740cf13397a 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. From e66be910051eaf60641456b44ea0a840aedd81b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 5 Jul 2024 10:05:04 +0200 Subject: [PATCH 239/261] Backport "Harmonize CI configuration" This commit applies the harmonization changes in eacfd77 whilst keeping the doc attributes specific changes to 6.0.x --- .github/actions/build/action.yml | 56 +++++++++++++++++++ .../actions/prepare-gradle-build/action.yml | 49 ++++++++++++++++ .github/actions/send-notification/action.yml | 2 +- .../workflows/build-and-deploy-snapshot.yml | 48 ++++++---------- .github/workflows/ci.yml | 48 ++++------------ ...dation.yml => validate-gradle-wrapper.yml} | 6 +- 6 files changed, 136 insertions(+), 73 deletions(-) create mode 100644 .github/actions/build/action.yml create mode 100644 .github/actions/prepare-gradle-build/action.yml rename .github/workflows/{gradle-wrapper-validation.yml => validate-gradle-wrapper.yml} (57%) diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 000000000000..5bd860139947 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,56 @@ +name: 'Build' +description: 'Builds the project, optionally publishing it to a local deployment repository' +inputs: + java-version: + required: false + default: '17' + description: 'The Java version to compile and test with' + java-distribution: + required: false + default: 'liberica' + description: 'The Java distribution to use for the build' + java-toolchain: + required: false + default: 'false' + description: 'Whether a Java toolchain should be used' + publish: + required: false + default: 'false' + description: 'Whether to publish artifacts ready for deployment to Artifactory' + develocity-access-key: + required: false + description: 'The access key for authentication with ge.spring.io' +outputs: + build-scan-url: + description: 'The URL, if any, of the build scan produced by the build' + value: ${{ (inputs.publish == 'true' && steps.publish.outputs.build-scan-url) || steps.build.outputs.build-scan-url }} + version: + description: 'The version that was built' + value: ${{ steps.read-version.outputs.version }} +runs: + using: composite + steps: + - name: Prepare Gradle Build + uses: ./.github/actions/prepare-gradle-build + with: + develocity-access-key: ${{ inputs.develocity-access-key }} + java-version: ${{ inputs.java-version }} + java-distribution: ${{ inputs.java-distribution }} + java-toolchain: ${{ inputs.java-toolchain }} + - name: Build + id: build + if: ${{ inputs.publish == 'false' }} + shell: bash + run: ./gradlew check antora + - name: Publish + id: publish + if: ${{ inputs.publish == 'true' }} + shell: bash + run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository + - name: Read Version From gradle.properties + id: read-version + shell: bash + run: | + version=$(sed -n 's/version=\(.*\)/\1/p' gradle.properties) + echo "Version is $version" + echo "version=$version" >> $GITHUB_OUTPUT diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml new file mode 100644 index 000000000000..0505951dc36e --- /dev/null +++ b/.github/actions/prepare-gradle-build/action.yml @@ -0,0 +1,49 @@ +name: 'Prepare Gradle Build' +description: 'Prepares a Gradle build. Sets up Java and Gradle and configures Gradle properties' +inputs: + java-version: + required: false + default: '17' + description: 'The Java version to use for the build' + java-distribution: + required: false + default: 'liberica' + description: 'The Java distribution to use for the build' + java-toolchain: + required: false + default: 'false' + description: 'Whether a Java toolchain should be used' + develocity-access-key: + required: false + description: 'The access key for authentication with ge.spring.io' +runs: + using: composite + steps: + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: ${{ inputs.java-distribution }} + java-version: | + ${{ inputs.java-version }} + ${{ inputs.java-toolchain == 'true' && '17' || '' }} + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + with: + cache-read-only: false + develocity-access-key: ${{ inputs.develocity-access-key }} + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties + echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties + - name: Configure Toolchain Properties + if: ${{ inputs.java-toolchain == 'true' }} + shell: bash + run: | + echo toolchainVersion=${{ inputs.java-version }} >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties + echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $HOME/.gradle/gradle.properties diff --git a/.github/actions/send-notification/action.yml b/.github/actions/send-notification/action.yml index 9582d44ed154..d1389776397a 100644 --- a/.github/actions/send-notification/action.yml +++ b/.github/actions/send-notification/action.yml @@ -1,4 +1,4 @@ -name: Send notification +name: Send Notification description: Sends a Google Chat message as a notification of the job's outcome inputs: webhook-url: diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 0b9103ff357c..3c48245a4184 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -1,44 +1,28 @@ -name: Build and deploy snapshot +name: Build and Deploy Snapshot on: push: branches: - 6.0.x +permissions: + actions: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: build-and-deploy-snapshot: - if: ${{ github.repository == 'spring-projects/spring-framework' }} - name: Build and deploy snapshot + name: Build and Deploy Snapshot runs-on: ubuntu-latest + if: ${{ github.repository == 'spring-projects/spring-framework' }} steps: - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: 'liberica' - java-version: 17 - - name: Check out code + - name: Check Out Code uses: actions/checkout@v4 - - name: Set up Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build with: - cache-read-only: false - - name: Configure Gradle properties - shell: bash - run: | - mkdir -p $HOME/.gradle - echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties - echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties - echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties - echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties - - name: Build and publish - id: build - env: - CI: 'true' - GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + publish: true - name: Deploy - uses: spring-io/artifactory-deploy-action@v0.0.1 + uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 with: uri: 'https://repo.spring.io' username: ${{ secrets.ARTIFACTORY_USERNAME }} @@ -53,11 +37,13 @@ jobs: /**/framework-docs-*-docs.zip::zip.type=docs /**/framework-docs-*-dist.zip::zip.type=dist /**/framework-docs-*-schema.zip::zip.type=schema - - name: Send notification + - name: Send Notification uses: ./.github/actions/send-notification if: always() with: webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} status: ${{ job.status }} - build-scan-url: ${{ steps.build.outputs.build-scan-url }} - run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} \ No newline at end of file + build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }} + run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0622990c7de2..47a59ea4c6dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: ci: + name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' + runs-on: ${{ matrix.os.id }} if: ${{ github.repository == 'spring-projects/spring-framework' }} strategy: matrix: @@ -23,56 +25,28 @@ jobs: name: Linux java: version: 17 - name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' - runs-on: ${{ matrix.os.id }} steps: - - name: Set up Java - uses: actions/setup-java@v4 - with: - distribution: 'liberica' - java-version: | - ${{ matrix.java.version }} - ${{ matrix.java.toolchain && '17' || '' }} - name: Prepare Windows runner if: ${{ runner.os == 'Windows' }} run: | git config --global core.autocrlf true git config --global core.longPaths true Stop-Service -name Docker - - name: Check out code + - name: Check Out Code uses: actions/checkout@v4 - - name: Set up Gradle - uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 - with: - cache-read-only: false - - name: Configure Gradle properties - shell: bash - run: | - mkdir -p $HOME/.gradle - echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties - echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties - echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties - echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties - - name: Configure toolchain properties - if: ${{ matrix.java.toolchain }} - shell: bash - run: | - echo toolchainVersion=${{ matrix.java.version }} >> $HOME/.gradle/gradle.properties - echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties - echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties - echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', matrix.java.version) }} >> $HOME/.gradle/gradle.properties - name: Build id: build - env: - CI: 'true' - GRADLE_ENTERPRISE_URL: 'https://ge.spring.io' - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - run: ./gradlew check antora - - name: Send notification + uses: ./.github/actions/build + with: + java-version: ${{ matrix.java.version }} + java-distribution: ${{ matrix.java.distribution || 'liberica' }} + java-toolchain: ${{ matrix.java.toolchain }} + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + - name: Send Notification uses: ./.github/actions/send-notification if: always() with: webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} status: ${{ job.status }} build-scan-url: ${{ steps.build.outputs.build-scan-url }} - run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }} \ No newline at end of file + run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/validate-gradle-wrapper.yml similarity index 57% rename from .github/workflows/gradle-wrapper-validation.yml rename to .github/workflows/validate-gradle-wrapper.yml index cf2c086a063c..e1629a5f5fe1 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/validate-gradle-wrapper.yml @@ -1,13 +1,11 @@ name: "Validate Gradle Wrapper" on: [push, pull_request] - permissions: contents: read - jobs: validation: - name: "Validation" + name: "Validate Gradle Wrapper" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/actions/wrapper-validation@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 From 84efba682d97f0645a12a9f8e63b4c503882d24d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 8 Jul 2024 23:25:34 +0200 Subject: [PATCH 240/261] Upgrade to Groovy 4.0.22, Jetty 11.0.22, Undertow 2.3.14 Includes downgrade to Awaitility 4.2.0 (aligned with 6.1.x branch) --- framework-platform/framework-platform.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 3808e4016d55..d0679158ef98 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -13,9 +13,9 @@ dependencies { api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2022.0.20")) api(platform("io.rsocket:rsocket-bom:1.1.3")) - api(platform("org.apache.groovy:groovy-bom:4.0.21")) + api(platform("org.apache.groovy:groovy-bom:4.0.22")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) - api(platform("org.eclipse.jetty:jetty-bom:11.0.20")) + api(platform("org.eclipse.jetty:jetty-bom:11.0.22")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0")) api(platform("org.junit:junit-bom:5.9.3")) @@ -54,9 +54,9 @@ dependencies { api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") api("io.reactivex.rxjava3:rxjava:3.1.8") api("io.smallrye.reactive:mutiny:1.10.0") - api("io.undertow:undertow-core:2.3.13.Final") - api("io.undertow:undertow-servlet:2.3.13.Final") - api("io.undertow:undertow-websockets-jsr:2.3.13.Final") + api("io.undertow:undertow-core:2.3.14.Final") + api("io.undertow:undertow-servlet:2.3.14.Final") + api("io.undertow:undertow-websockets-jsr:2.3.14.Final") api("io.vavr:vavr:0.10.4") api("jakarta.activation:jakarta.activation-api:2.0.1") api("jakarta.annotation:jakarta.annotation-api:2.0.0") @@ -107,7 +107,7 @@ dependencies { api("org.aspectj:aspectjtools:1.9.22.1") api("org.aspectj:aspectjweaver:1.9.22.1") api("org.assertj:assertj-core:3.24.2") - api("org.awaitility:awaitility:4.2.1") + api("org.awaitility:awaitility:4.2.0") api("org.bouncycastle:bcpkix-jdk18on:1.72") api("org.codehaus.jettison:jettison:1.5.4") api("org.dom4j:dom4j:2.1.4") From be7029c35dca4d70de15ea097649799aec19ac88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 10 Jul 2024 12:26:03 +0200 Subject: [PATCH 241/261] Retain previous factory method in case of nested invocation with AOT This commit harmonizes the invocation of a bean supplier with what SimpleInstantiationStrategy does. Previously, the current factory method was set to `null` once the invocation completes. This did not take into account recursive scenarios where an instance supplier triggers another instance supplier. For consistency, the thread local is removed now if we attempt to set the current method to null. SimpleInstantiationStrategy itself uses the shortcut to align the code as much as possible. Closes gh-33185 --- .../factory/aot/BeanInstanceSupplier.java | 5 ++- .../support/SimpleInstantiationStrategy.java | 23 +++++----- .../aot/BeanInstanceSupplierTests.java | 45 ++++++++++++++++++- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java index 265e939e01c0..5c2bb461fd18 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -209,12 +209,13 @@ private T invokeBeanSupplier(Executable executable, ThrowingSupplier beanSupp if (!(executable instanceof Method method)) { return beanSupplier.get(); } + Method priorInvokedFactoryMethod = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); try { SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(method); return beanSupplier.get(); } finally { - SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(null); + SimpleInstantiationStrategy.setCurrentlyInvokedFactoryMethod(priorInvokedFactoryMethod); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java index 0fc15c5a3539..56f5dffe14b0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,12 +54,18 @@ public static Method getCurrentlyInvokedFactoryMethod() { } /** - * Set the factory method currently being invoked or {@code null} to reset. + * Set the factory method currently being invoked or {@code null} to remove + * the current value, if any. * @param method the factory method currently being invoked or {@code null} * @since 6.0 */ public static void setCurrentlyInvokedFactoryMethod(@Nullable Method method) { - currentlyInvokedFactoryMethod.set(method); + if (method != null) { + currentlyInvokedFactoryMethod.set(method); + } + else { + currentlyInvokedFactoryMethod.remove(); + } } @@ -133,9 +139,9 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean try { ReflectionUtils.makeAccessible(factoryMethod); - Method priorInvokedFactoryMethod = currentlyInvokedFactoryMethod.get(); + Method priorInvokedFactoryMethod = getCurrentlyInvokedFactoryMethod(); try { - currentlyInvokedFactoryMethod.set(factoryMethod); + setCurrentlyInvokedFactoryMethod(factoryMethod); Object result = factoryMethod.invoke(factoryBean, args); if (result == null) { result = new NullBean(); @@ -143,12 +149,7 @@ public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, Bean return result; } finally { - if (priorInvokedFactoryMethod != null) { - currentlyInvokedFactoryMethod.set(priorInvokedFactoryMethod); - } - else { - currentlyInvokedFactoryMethod.remove(); - } + setCurrentlyInvokedFactoryMethod(priorInvokedFactoryMethod); } } catch (IllegalArgumentException ex) { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java index 422de9d4420d..ec25e328f4c3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Stream; @@ -51,6 +52,7 @@ import org.springframework.beans.factory.support.InstanceSupplier; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.support.SimpleInstantiationStrategy; import org.springframework.core.env.Environment; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; @@ -292,6 +294,33 @@ void getNestedWithNoGeneratorUsesReflection(Source source) throws Exception { assertThat(instance).isEqualTo("1"); } + @Test // gh-33180 + void getWithNestedInvocationRetainsFactoryMethod() throws Exception { + AtomicReference testMethodReference = new AtomicReference<>(); + AtomicReference anotherMethodReference = new AtomicReference<>(); + + BeanInstanceSupplier nestedInstanceSupplier = BeanInstanceSupplier + .forFactoryMethod(AnotherTestStringFactory.class, "another") + .withGenerator(registeredBean -> { + anotherMethodReference.set(SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod()); + return "Another"; + }); + RegisteredBean nestedRegisteredBean = new Source(String.class, nestedInstanceSupplier).registerBean(this.beanFactory); + BeanInstanceSupplier instanceSupplier = BeanInstanceSupplier + .forFactoryMethod(TestStringFactory.class, "test") + .withGenerator(registeredBean -> { + Object nested = nestedInstanceSupplier.get(nestedRegisteredBean); + testMethodReference.set(SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod()); + return "custom" + nested; + }); + RegisteredBean registeredBean = new Source(String.class, instanceSupplier).registerBean(this.beanFactory); + Object value = instanceSupplier.get(registeredBean); + + assertThat(value).isEqualTo("customAnother"); + assertThat(testMethodReference.get()).isEqualTo(instanceSupplier.getFactoryMethod()); + assertThat(anotherMethodReference.get()).isEqualTo(nestedInstanceSupplier.getFactoryMethod()); + } + @Test void resolveArgumentsWithNoArgConstructor() { RootBeanDefinition beanDefinition = new RootBeanDefinition( @@ -934,4 +963,18 @@ static class MethodOnInterfaceImpl implements MethodOnInterface { } + static class TestStringFactory { + + String test() { + return "test"; + } + } + + static class AnotherTestStringFactory { + + String another() { + return "another"; + } + } + } From fd7e7633d4bd5928416e71d2885fbe8eea165150 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 10 Jul 2024 15:45:02 +0200 Subject: [PATCH 242/261] Polishing --- .../ROOT/pages/data-access/jdbc/advanced.adoc | 23 ++++---- ...opedProxyBeanRegistrationAotProcessor.java | 41 ++++++-------- ...eanRegistrationCodeFragmentsDecorator.java | 27 +++++----- .../DefaultBeanRegistrationCodeFragments.java | 54 ++++++++----------- .../ConfigurationClassPostProcessor.java | 18 +++---- 5 files changed, 69 insertions(+), 94 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc index 04533c9cb822..6aeb504a0f27 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/advanced.adoc @@ -211,19 +211,20 @@ the JDBC driver. If the count is not available, the JDBC driver returns a value ==== In such a scenario, with automatic setting of values on an underlying `PreparedStatement`, the corresponding JDBC type for each value needs to be derived from the given Java type. -While this usually works well, there is a potential for issues (for example, with Map-contained -`null` values). Spring, by default, calls `ParameterMetaData.getParameterType` in such a -case, which can be expensive with your JDBC driver. You should use a recent driver +While this usually works well, there is a potential for issues (for example, with +Map-contained `null` values). Spring, by default, calls `ParameterMetaData.getParameterType` +in such a case, which can be expensive with your JDBC driver. You should use a recent driver version and consider setting the `spring.jdbc.getParameterType.ignore` property to `true` (as a JVM system property or via the -xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) if you encounter -a performance issue (as reported on Oracle 12c, JBoss, and PostgreSQL). - -Alternatively, you might consider specifying the corresponding JDBC types explicitly, -either through a `BatchPreparedStatementSetter` (as shown earlier), through an explicit type -array given to a `List` based call, through `registerSqlType` calls on a -custom `MapSqlParameterSource` instance, or through a `BeanPropertySqlParameterSource` -that derives the SQL type from the Java-declared property type even for a null value. +xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism) +if you encounter a specific performance issue for your application. + +Alternatively, you could consider specifying the corresponding JDBC types explicitly, +either through a `BatchPreparedStatementSetter` (as shown earlier), through an explicit +type array given to a `List` based call, through `registerSqlType` calls on a +custom `MapSqlParameterSource` instance, through a `BeanPropertySqlParameterSource` +that derives the SQL type from the Java-declared property type even for a null value, or +through providing individual `SqlParameterValue` instances instead of plain null values. ==== diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java index dbf2df4fa192..05906fd90323 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,6 +54,7 @@ class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProc @Override + @Nullable public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); if (beanClass.equals(ScopedProxyFactoryBean.class)) { @@ -79,8 +80,8 @@ private String getTargetBeanName(BeanDefinition beanDefinition) { } @Nullable - private BeanDefinition getTargetBeanDefinition(ConfigurableBeanFactory beanFactory, - @Nullable String targetBeanName) { + private BeanDefinition getTargetBeanDefinition( + ConfigurableBeanFactory beanFactory, @Nullable String targetBeanName) { if (targetBeanName != null && beanFactory.containsBean(targetBeanName)) { return beanFactory.getMergedBeanDefinition(targetBeanName); @@ -123,42 +124,32 @@ public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationConte @Override public CodeBlock generateSetBeanDefinitionPropertiesCode( - GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate attributeFilter) { - RootBeanDefinition processedBeanDefinition = new RootBeanDefinition( - beanDefinition); - processedBeanDefinition - .setTargetType(this.targetBeanDefinition.getResolvableType()); - processedBeanDefinition.getPropertyValues() - .removePropertyValue("targetBeanName"); + RootBeanDefinition processedBeanDefinition = new RootBeanDefinition(beanDefinition); + processedBeanDefinition.setTargetType(this.targetBeanDefinition.getResolvableType()); + processedBeanDefinition.getPropertyValues().removePropertyValue("targetBeanName"); return super.generateSetBeanDefinitionPropertiesCode(generationContext, beanRegistrationCode, processedBeanDefinition, attributeFilter); } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, - Executable constructorOrFactoryMethod, - boolean allowDirectSupplierShortcut) { + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut) { GeneratedMethod generatedMethod = beanRegistrationCode.getMethods() .add("getScopedProxyInstance", method -> { - method.addJavadoc( - "Create the scoped proxy bean instance for '$L'.", + method.addJavadoc("Create the scoped proxy bean instance for '$L'.", this.registeredBean.getBeanName()); method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); method.returns(ScopedProxyFactoryBean.class); - method.addParameter(RegisteredBean.class, - REGISTERED_BEAN_PARAMETER_NAME); + method.addParameter(RegisteredBean.class, REGISTERED_BEAN_PARAMETER_NAME); method.addStatement("$T factory = new $T()", - ScopedProxyFactoryBean.class, - ScopedProxyFactoryBean.class); - method.addStatement("factory.setTargetBeanName($S)", - this.targetBeanName); - method.addStatement( - "factory.setBeanFactory($L.getBeanFactory())", + ScopedProxyFactoryBean.class, ScopedProxyFactoryBean.class); + method.addStatement("factory.setTargetBeanName($S)", this.targetBeanName); + method.addStatement("factory.setBeanFactory($L.getBeanFactory())", REGISTERED_BEAN_PARAMETER_NAME); method.addStatement("return factory"); }); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java index e4ff961262e1..37f610f5af1f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationCodeFragmentsDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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,40 +59,39 @@ public ClassName getTarget(RegisteredBean registeredBean, Executable constructor public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationContext, ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) { - return this.delegate.generateNewBeanDefinitionCode(generationContext, - beanType, beanRegistrationCode); + return this.delegate.generateNewBeanDefinitionCode(generationContext, beanType, beanRegistrationCode); } @Override - public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, - Predicate attributeFilter) { + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { return this.delegate.generateSetBeanDefinitionPropertiesCode( generationContext, beanRegistrationCode, beanDefinition, attributeFilter); } @Override - public CodeBlock generateSetBeanInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, CodeBlock instanceSupplierCode, - List postProcessors) { + public CodeBlock generateSetBeanInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + CodeBlock instanceSupplierCode, List postProcessors) { return this.delegate.generateSetBeanInstanceSupplierCode(generationContext, beanRegistrationCode, instanceSupplierCode, postProcessors); } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, - boolean allowDirectSupplierShortcut) { + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut) { return this.delegate.generateInstanceSupplierCode(generationContext, beanRegistrationCode, constructorOrFactoryMethod, allowDirectSupplierShortcut); } @Override - public CodeBlock generateReturnCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode) { + public CodeBlock generateReturnCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { return this.delegate.generateReturnCode(generationContext, beanRegistrationCode); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java index d5281ef6caf3..c3978fe3e395 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -41,8 +41,7 @@ import org.springframework.util.ClassUtils; /** - * Internal {@link BeanRegistrationCodeFragments} implementation used by - * default. + * Internal {@link BeanRegistrationCodeFragments} implementation used by default. * * @author Phillip Webb */ @@ -55,8 +54,8 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme private final BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory; - DefaultBeanRegistrationCodeFragments(BeanRegistrationsCode beanRegistrationsCode, - RegisteredBean registeredBean, + DefaultBeanRegistrationCodeFragments( + BeanRegistrationsCode beanRegistrationsCode, RegisteredBean registeredBean, BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory) { this.beanRegistrationsCode = beanRegistrationsCode; @@ -66,9 +65,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme @Override - public ClassName getTarget(RegisteredBean registeredBean, - Executable constructorOrFactoryMethod) { - + public ClassName getTarget(RegisteredBean registeredBean, Executable constructorOrFactoryMethod) { Class target = extractDeclaringClass(registeredBean.getBeanType(), constructorOrFactoryMethod); while (target.getName().startsWith("java.") && registeredBean.isInnerBean()) { RegisteredBean parent = registeredBean.getParent(); @@ -80,9 +77,8 @@ public ClassName getTarget(RegisteredBean registeredBean, private Class extractDeclaringClass(ResolvableType beanType, Executable executable) { Class declaringClass = ClassUtils.getUserClass(executable.getDeclaringClass()); - if (executable instanceof Constructor - && AccessControl.forMember(executable).isPublic() - && FactoryBean.class.isAssignableFrom(declaringClass)) { + if (executable instanceof Constructor && AccessControl.forMember(executable).isPublic() && + FactoryBean.class.isAssignableFrom(declaringClass)) { return extractTargetClassFromFactoryBean(declaringClass, beanType); } return executable.getDeclaringClass(); @@ -91,8 +87,7 @@ private Class extractDeclaringClass(ResolvableType beanType, Executable execu /** * Extract the target class of a public {@link FactoryBean} based on its * constructor. If the implementation does not resolve the target class - * because it itself uses a generic, attempt to extract it from the - * bean type. + * because it itself uses a generic, attempt to extract it from the bean type. * @param factoryBeanType the factory bean type * @param beanType the bean type * @return the target class to use @@ -113,17 +108,15 @@ public CodeBlock generateNewBeanDefinitionCode(GenerationContext generationConte ResolvableType beanType, BeanRegistrationCode beanRegistrationCode) { CodeBlock.Builder code = CodeBlock.builder(); - RootBeanDefinition mergedBeanDefinition = this.registeredBean.getMergedBeanDefinition(); - Class beanClass = (mergedBeanDefinition.hasBeanClass() - ? ClassUtils.getUserClass(mergedBeanDefinition.getBeanClass()) : null); + RootBeanDefinition mbd = this.registeredBean.getMergedBeanDefinition(); + Class beanClass = (mbd.hasBeanClass() ? ClassUtils.getUserClass(mbd.getBeanClass()) : null); CodeBlock beanClassCode = generateBeanClassCode( beanRegistrationCode.getClassName().packageName(), (beanClass != null ? beanClass : beanType.toClass())); code.addStatement("$T $L = new $T($L)", RootBeanDefinition.class, BEAN_DEFINITION_VARIABLE, RootBeanDefinition.class, beanClassCode); if (targetTypeNecessary(beanType, beanClass)) { - code.addStatement("$L.setTargetType($L)", BEAN_DEFINITION_VARIABLE, - generateBeanTypeCode(beanType)); + code.addStatement("$L.setTargetType($L)", BEAN_DEFINITION_VARIABLE, generateBeanTypeCode(beanType)); } return code.build(); } @@ -148,8 +141,7 @@ private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class if (beanType.hasGenerics()) { return true; } - if (beanClass != null - && this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null) { + if (beanClass != null && this.registeredBean.getMergedBeanDefinition().getFactoryMethodName() != null) { return true; } return (beanClass != null && !beanType.toClass().equals(beanClass)); @@ -157,9 +149,8 @@ private boolean targetTypeNecessary(ResolvableType beanType, @Nullable Class @Override public CodeBlock generateSetBeanDefinitionPropertiesCode( - GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, - Predicate attributeFilter) { + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { return new BeanDefinitionPropertiesCodeGenerator( generationContext.getRuntimeHints(), attributeFilter, @@ -169,9 +160,7 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode( } @Nullable - protected CodeBlock generateValueCode(GenerationContext generationContext, - String name, Object value) { - + protected CodeBlock generateValueCode(GenerationContext generationContext, String name, Object value) { RegisteredBean innerRegisteredBean = getInnerRegisteredBean(value); if (innerRegisteredBean != null) { BeanDefinitionMethodGenerator methodGenerator = this.beanDefinitionMethodGeneratorFactory @@ -197,9 +186,8 @@ private RegisteredBean getInnerRegisteredBean(Object value) { @Override public CodeBlock generateSetBeanInstanceSupplierCode( - GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, CodeBlock instanceSupplierCode, - List postProcessors) { + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + CodeBlock instanceSupplierCode, List postProcessors) { CodeBlock.Builder code = CodeBlock.builder(); if (postProcessors.isEmpty()) { @@ -219,8 +207,8 @@ public CodeBlock generateSetBeanInstanceSupplierCode( } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut) { return new InstanceSupplierCodeGenerator(generationContext, @@ -229,8 +217,8 @@ public CodeBlock generateInstanceSupplierCode(GenerationContext generationContex } @Override - public CodeBlock generateReturnCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode) { + public CodeBlock generateReturnCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { CodeBlock.Builder code = CodeBlock.builder(); code.addStatement("return $L", BEAN_DEFINITION_VARIABLE); 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 4eb4a5809f14..6e70c3d0816b 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 @@ -737,12 +737,7 @@ private CodeBlock generatePropertySourceDescriptorCode(PropertySourceDescriptor } private CodeBlock handleNull(@Nullable Object value, Supplier nonNull) { - if (value == null) { - return CodeBlock.of("null"); - } - else { - return nonNull.get(); - } + return (value == null ? CodeBlock.of("null") : nonNull.get()); } } @@ -757,8 +752,9 @@ public ConfigurationClassProxyBeanRegistrationCodeFragments(BeanRegistrationCode } @Override - public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, RootBeanDefinition beanDefinition, Predicate attributeFilter) { + public CodeBlock generateSetBeanDefinitionPropertiesCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + RootBeanDefinition beanDefinition, Predicate attributeFilter) { CodeBlock.Builder code = CodeBlock.builder(); code.add(super.generateSetBeanDefinitionPropertiesCode(generationContext, @@ -769,9 +765,9 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener } @Override - public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, - boolean allowDirectSupplierShortcut) { + public CodeBlock generateInstanceSupplierCode( + GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode, + Executable constructorOrFactoryMethod, boolean allowDirectSupplierShortcut) { Executable executableToUse = proxyExecutable(generationContext.getRuntimeHints(), constructorOrFactoryMethod); return super.generateInstanceSupplierCode(generationContext, beanRegistrationCode, From a2a793e324233af48f892ccb1031b039e5fb3f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 11 Jul 2024 16:48:05 +0200 Subject: [PATCH 243/261] Release from GitHub Actions Closes gh-33201 --- .../actions/create-github-release/action.yml | 23 +++++ .../changelog-generator.yml | 28 ++++++ .../actions/sync-to-maven-central/action.yml | 50 +++++++++++ .../sync-to-maven-central/artifacts.spec | 20 +++++ .../workflows/build-and-deploy-snapshot.yml | 13 ++- .github/workflows/release.yml | 90 +++++++++++++++++++ .github/workflows/verify.yml | 71 +++++++++++++++ 7 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 .github/actions/create-github-release/action.yml create mode 100644 .github/actions/create-github-release/changelog-generator.yml create mode 100644 .github/actions/sync-to-maven-central/action.yml create mode 100644 .github/actions/sync-to-maven-central/artifacts.spec create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/verify.yml diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml new file mode 100644 index 000000000000..0354737e5dfb --- /dev/null +++ b/.github/actions/create-github-release/action.yml @@ -0,0 +1,23 @@ +name: Create GitHub Release +description: Create the release on GitHub with a changelog +inputs: + milestone: + description: 'Name of the GitHub milestone for which a release will be created' + required: true + token: + description: 'Token to use for authentication with GitHub' + required: true +runs: + using: composite + steps: + - name: Generate Changelog + uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 + with: + milestone: ${{ inputs.milestone }} + token: ${{ inputs.token }} + config-file: .github/actions/create-github-release/changelog-generator.yml + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ inputs.token }} + shell: bash + run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md diff --git a/.github/actions/create-github-release/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator.yml new file mode 100644 index 000000000000..725c40966679 --- /dev/null +++ b/.github/actions/create-github-release/changelog-generator.yml @@ -0,0 +1,28 @@ +changelog: + repository: spring-projects/spring-framework + sections: + - title: ":star: New Features" + labels: + - "type: enhancement" + - title: ":lady_beetle: Bug Fixes" + labels: + - "type: bug" + - "type: regression" + - title: ":notebook_with_decorative_cover: Documentation" + labels: + - "type: documentation" + - title: ":hammer: Dependency Upgrades" + sort: "title" + labels: + - "type: dependency-upgrade" + contributors: + exclude: + names: + - "bclozel" + - "jhoeller" + - "poutsma" + - "rstoyanchev" + - "sbrannen" + - "sdeleuze" + - "simonbasle" + - "snicoll" diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml new file mode 100644 index 000000000000..71d17baf73c7 --- /dev/null +++ b/.github/actions/sync-to-maven-central/action.yml @@ -0,0 +1,50 @@ +name: Sync to Maven Central +description: Syncs a release to Maven Central and waits for it to be available for use +inputs: + jfrog-cli-config-token: + description: 'Config token for the JFrog CLI' + required: true + spring-framework-version: + description: 'The version of Spring Framework that is being synced to Central' + required: true + ossrh-s01-token-username: + description: 'Username for authentication with s01.oss.sonatype.org' + required: true + ossrh-s01-token-password: + description: 'Password for authentication with s01.oss.sonatype.org' + required: true + ossrh-s01-staging-profile: + description: 'Staging profile to use when syncing to Central' + required: true +runs: + using: composite + steps: + - name: Set Up JFrog CLI + uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.1.2 + env: + JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }} + - name: Download Release Artifacts + shell: bash + run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}' + - name: Sync + uses: spring-io/nexus-sync-action@42477a2230a2f694f9eaa4643fa9e76b99b7ab84 # v0.0.1 + with: + username: ${{ inputs.ossrh-s01-token-username }} + password: ${{ inputs.ossrh-s01-token-password }} + staging-profile-name: ${{ inputs.ossrh-s01-staging-profile }} + create: true + upload: true + close: true + release: true + generate-checksums: true + - name: Await + shell: bash + run: | + url=${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }} + echo "Waiting for $url" + until curl --fail --head --silent $url > /dev/null + do + echo "." + sleep 60 + done + echo "$url is available" diff --git a/.github/actions/sync-to-maven-central/artifacts.spec b/.github/actions/sync-to-maven-central/artifacts.spec new file mode 100644 index 000000000000..b3e338406973 --- /dev/null +++ b/.github/actions/sync-to-maven-central/artifacts.spec @@ -0,0 +1,20 @@ +{ + "files": [ + { + "aql": { + "items.find": { + "$and": [ + { + "@build.name": "${buildName}", + "@build.number": "${buildNumber}", + "path": { + "$nmatch": "org/springframework/framework-docs/*" + } + } + ] + } + }, + "target": "nexus/" + } + ] +} diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 3c48245a4184..68147a08c9d5 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -27,7 +27,7 @@ jobs: uri: 'https://repo.spring.io' username: ${{ secrets.ARTIFACTORY_USERNAME }} password: ${{ secrets.ARTIFACTORY_PASSWORD }} - build-name: ${{ format('spring-framework-{0}', github.ref_name)}} + build-name: 'spring-framework-6.0.x' repository: 'libs-snapshot-local' folder: 'deployment-repository' signing-key: ${{ secrets.GPG_PRIVATE_KEY }} @@ -47,3 +47,14 @@ jobs: run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }} outputs: version: ${{ steps.build-and-publish.outputs.version }} + verify: + name: Verify + needs: build-and-deploy-snapshot + uses: ./.github/workflows/verify.yml + secrets: + google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + with: + version: ${{ needs.build-and-deploy-snapshot.outputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..71503639d4f4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release +on: + push: + tags: + - v6.0.[0-9]+ +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-stage-release: + if: ${{ github.repository == 'spring-projects/spring-framework' }} + name: Build and Stage Release + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + publish: true + - name: Stage Release + uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 + with: + build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}} + folder: 'deployment-repository' + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository: 'libs-staging-local' + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} + verify: + name: Verify + needs: build-and-stage-release + uses: ./.github/workflows/verify.yml + with: + staging: true + version: ${{ needs.build-and-stage-release.outputs.version }} + secrets: + google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + sync-to-maven-central: + name: Sync to Maven Central + needs: + - build-and-stage-release + - verify + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Sync to Maven Central + uses: ./.github/actions/sync-to-maven-central + with: + jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }} + ossrh-s01-staging-profile: ${{ secrets.OSSRH_S01_STAGING_PROFILE }} + ossrh-s01-token-password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + ossrh-s01-token-username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }} + promote-release: + name: Promote Release + needs: + - build-and-stage-release + - sync-to-maven-central + runs-on: ubuntu-latest + steps: + - name: Set up JFrog CLI + uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.1.2 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Promote build + run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local + create-github-release: + name: Create GitHub Release + needs: + - build-and-stage-release + - promote-release + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Create GitHub Release + uses: ./.github/actions/create-github-release + with: + milestone: ${{ needs.build-and-stage-release.outputs.version }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 000000000000..b38288a1723b --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,71 @@ +name: Verify +on: + workflow_call: + inputs: + version: + required: true + type: string + staging: + required: false + default: false + type: boolean + secrets: + repository-username: + required: false + repository-password: + required: false + google-chat-webhook-url: + required: true + token: + required: true +jobs: + verify: + name: Verify + runs-on: ubuntu-latest + steps: + - name: Check Out Release Verification Tests + uses: actions/checkout@v4 + with: + repository: spring-projects/spring-framework-release-verification + ref: 'v0.0.2' + token: ${{ secrets.token }} + - name: Check Out Send Notification Action + uses: actions/checkout@v4 + with: + path: spring-framework + sparse-checkout: .github/actions/send-notification + - name: Set Up Java + uses: actions/setup-java@v4 + with: + distribution: 'liberica' + java-version: 17 + - name: Set Up Gradle + uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + with: + cache-read-only: false + - name: Configure Gradle Properties + shell: bash + run: | + mkdir -p $HOME/.gradle + echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties + - name: Run Release Verification Tests + env: + RVT_VERSION: ${{ inputs.version }} + RVT_RELEASE_TYPE: oss + RVT_STAGING: ${{ inputs.staging }} + RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.repository-username }} + RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.repository-password }} + run: ./gradlew spring-framework-release-verification-tests:test + - name: Upload Build Reports on Failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: build-reports + path: '**/build/reports/' + - name: Send Notification + uses: ./spring-framework/.github/actions/send-notification + if: failure() + with: + webhook-url: ${{ secrets.google-chat-webhook-url }} + status: ${{ job.status }} + run-name: ${{ format('{0} | Verification | {1}', github.ref_name, inputs.version) }} From 0376ebeaf305c6fe123fe7600add766071d972a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 11 Jul 2024 16:58:01 +0200 Subject: [PATCH 244/261] Remove concourse configuration now that CI is using GitHub Actions --- ci/README.adoc | 59 ------ ci/config/changelog-generator.yml | 29 --- ci/config/release-scripts.yml | 10 - ci/images/README.adoc | 21 --- ci/images/ci-image/Dockerfile | 10 - ci/images/get-jdk-url.sh | 11 -- ci/images/setup.sh | 40 ---- ci/parameters.yml | 10 - ci/pipeline.yml | 295 ------------------------------ ci/scripts/common.sh | 2 - ci/scripts/generate-changelog.sh | 12 -- ci/scripts/promote-version.sh | 17 -- ci/scripts/stage-version.sh | 50 ----- ci/tasks/build-ci-image.yml | 30 --- ci/tasks/generate-changelog.yml | 22 --- ci/tasks/promote-version.yml | 25 --- ci/tasks/stage-version.yml | 17 -- 17 files changed, 660 deletions(-) delete mode 100644 ci/README.adoc delete mode 100644 ci/config/changelog-generator.yml delete mode 100644 ci/config/release-scripts.yml delete mode 100644 ci/images/README.adoc delete mode 100644 ci/images/ci-image/Dockerfile delete mode 100755 ci/images/get-jdk-url.sh delete mode 100755 ci/images/setup.sh delete mode 100644 ci/parameters.yml delete mode 100644 ci/pipeline.yml delete mode 100644 ci/scripts/common.sh delete mode 100755 ci/scripts/generate-changelog.sh delete mode 100755 ci/scripts/promote-version.sh delete mode 100755 ci/scripts/stage-version.sh delete mode 100644 ci/tasks/build-ci-image.yml delete mode 100755 ci/tasks/generate-changelog.yml delete mode 100644 ci/tasks/promote-version.yml delete mode 100644 ci/tasks/stage-version.yml diff --git a/ci/README.adoc b/ci/README.adoc deleted file mode 100644 index 89780a0b944e..000000000000 --- a/ci/README.adoc +++ /dev/null @@ -1,59 +0,0 @@ -== Spring Framework Concourse pipeline - -NOTE: CI is being migrated to GitHub Actions. - -The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks. -The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline -for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x[Spring Framework 6.0.x]. - -=== Setting up your development environment - -If you're part of the Spring Framework project on GitHub, you can get access to CI management features. -First, you need to go to https://ci.spring.io and install the client CLI for your platform (see bottom right of the screen). - -You can then login with the instance using: - -[source] ----- -$ fly -t spring login -n spring-framework -c https://ci.spring.io ----- - -Once logged in, you should get something like: - -[source] ----- -$ fly ts -name url team expiry -spring https://ci.spring.io spring-framework Wed, 25 Mar 2020 17:45:26 UTC ----- - -=== Pipeline configuration and structure - -The build pipelines are described in `pipeline.yml` file. - -This file is listing Concourse resources, i.e. build inputs and outputs such as container images, artifact repositories, source repositories, notification services, etc. - -It also describes jobs (a job is a sequence of inputs, tasks and outputs); jobs are organized by groups. - -The `pipeline.yml` definition contains `((parameters))` which are loaded from the `parameters.yml` file or from our https://docs.cloudfoundry.org/credhub/[credhub instance]. - -You'll find in this folder the following resources: - -* `pipeline.yml` the build pipeline -* `parameters.yml` the build parameters used for the pipeline -* `images/` holds the container images definitions used in this pipeline -* `scripts/` holds the build scripts that ship within the CI container images -* `tasks` contains the task definitions used in the main `pipeline.yml` - -=== Updating the build pipeline - -Updating files on the repository is not enough to update the build pipeline, as changes need to be applied. - -The pipeline can be deployed using the following command: - -[source] ----- -$ fly -t spring set-pipeline -p spring-framework-6.0.x -c ci/pipeline.yml -l ci/parameters.yml ----- - -NOTE: This assumes that you have credhub integration configured with the appropriate secrets. diff --git a/ci/config/changelog-generator.yml b/ci/config/changelog-generator.yml deleted file mode 100644 index 082f16ed566a..000000000000 --- a/ci/config/changelog-generator.yml +++ /dev/null @@ -1,29 +0,0 @@ -changelog: - repository: spring-projects/spring-framework - sections: - - title: ":star: New Features" - labels: - - "type: enhancement" - - title: ":lady_beetle: Bug Fixes" - labels: - - "type: bug" - - "type: regression" - - title: ":notebook_with_decorative_cover: Documentation" - labels: - - "type: documentation" - - title: ":hammer: Dependency Upgrades" - sort: "title" - labels: - - "type: dependency-upgrade" - contributors: - exclude: - names: - - "bclozel" - - "github-actions[bot]" - - "jhoeller" - - "poutsma" - - "rstoyanchev" - - "sbrannen" - - "sdeleuze" - - "simonbasle" - - "snicoll" diff --git a/ci/config/release-scripts.yml b/ci/config/release-scripts.yml deleted file mode 100644 index d31f8cba00dc..000000000000 --- a/ci/config/release-scripts.yml +++ /dev/null @@ -1,10 +0,0 @@ -logging: - level: - io.spring.concourse: DEBUG -spring: - main: - banner-mode: off -sonatype: - exclude: - - 'build-info\.json' - - '.*\.zip' diff --git a/ci/images/README.adoc b/ci/images/README.adoc deleted file mode 100644 index 6da9addd9ca5..000000000000 --- a/ci/images/README.adoc +++ /dev/null @@ -1,21 +0,0 @@ -== CI Images - -These images are used by CI to run the actual builds. - -To build the image locally run the following from this directory: - ----- -$ docker build --no-cache -f /Dockerfile . ----- - -For example - ----- -$ docker build --no-cache -f spring-framework-ci-image/Dockerfile . ----- - -To test run: - ----- -$ docker run -it --entrypoint /bin/bash ----- diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile deleted file mode 100644 index 18a98b34de84..000000000000 --- a/ci/images/ci-image/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM ubuntu:jammy-20240125 - -ADD setup.sh /setup.sh -ADD get-jdk-url.sh /get-jdk-url.sh -RUN ./setup.sh - -ENV JAVA_HOME /opt/openjdk/java17 -ENV JDK17 /opt/openjdk/java17 - -ENV PATH $JAVA_HOME/bin:$PATH diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh deleted file mode 100755 index 80c68d3dab7f..000000000000 --- a/ci/images/get-jdk-url.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -set -e - -case "$1" in - java17) - echo "https://github.com/bell-sw/Liberica/releases/download/17.0.10%2B13/bellsoft-jdk17.0.10+13-linux-amd64.tar.gz" - ;; - *) - echo $"Unknown java version" - exit 1 -esac diff --git a/ci/images/setup.sh b/ci/images/setup.sh deleted file mode 100755 index ebdc7f843800..000000000000 --- a/ci/images/setup.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -set -ex - -########################################################### -# UTILS -########################################################### - -export DEBIAN_FRONTEND=noninteractive -apt-get update -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.4/concourse-java.sh > /opt/concourse-java.sh - -########################################################### -# JAVA -########################################################### - -mkdir -p /opt/openjdk -pushd /opt/openjdk > /dev/null -for jdk in java17 -do - JDK_URL=$( /get-jdk-url.sh $jdk ) - mkdir $jdk - pushd $jdk > /dev/null - curl -L ${JDK_URL} | tar zx --strip-components=1 - test -f bin/java - test -f bin/javac - popd > /dev/null -done -popd - -########################################################### -# GRADLE ENTERPRISE -########################################################### -cd / -mkdir ~/.gradle -echo 'systemProp.user.name=concourse' > ~/.gradle/gradle.properties diff --git a/ci/parameters.yml b/ci/parameters.yml deleted file mode 100644 index 32cf44f8d5ce..000000000000 --- a/ci/parameters.yml +++ /dev/null @@ -1,10 +0,0 @@ -github-repo: "https://github.com/spring-projects/spring-framework.git" -github-repo-name: "spring-projects/spring-framework" -sonatype-staging-profile: "org.springframework" -docker-hub-organization: "springci" -artifactory-server: "https://repo.spring.io" -branch: "6.0.x" -milestone: "6.0.x" -build-name: "spring-framework" -pipeline-name: "spring-framework" -concourse-url: "https://ci.spring.io" diff --git a/ci/pipeline.yml b/ci/pipeline.yml deleted file mode 100644 index cb38e8950003..000000000000 --- a/ci/pipeline.yml +++ /dev/null @@ -1,295 +0,0 @@ -anchors: - git-repo-resource-source: &git-repo-resource-source - uri: ((github-repo)) - username: ((github-username)) - password: ((github-ci-release-token)) - branch: ((branch)) - gradle-enterprise-task-params: &gradle-enterprise-task-params - DEVELOCITY_ACCESS_KEY: ((gradle_enterprise_secret_access_key)) - sonatype-task-params: &sonatype-task-params - SONATYPE_USERNAME: ((s01-user-token)) - SONATYPE_PASSWORD: ((s01-user-token-password)) - SONATYPE_URL: ((sonatype-url)) - SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile)) - artifactory-task-params: &artifactory-task-params - ARTIFACTORY_SERVER: ((artifactory-server)) - ARTIFACTORY_USERNAME: ((artifactory-username)) - ARTIFACTORY_PASSWORD: ((artifactory-password)) - build-project-task-params: &build-project-task-params - BRANCH: ((branch)) - <<: *gradle-enterprise-task-params - docker-resource-source: &docker-resource-source - username: ((docker-hub-username)) - password: ((docker-hub-password)) - changelog-task-params: &changelog-task-params - name: generated-changelog/tag - tag: generated-changelog/tag - body: generated-changelog/changelog.md - github-task-params: &github-task-params - GITHUB_USERNAME: ((github-username)) - GITHUB_TOKEN: ((github-ci-release-token)) - -resource_types: -- name: registry-image - type: registry-image - source: - <<: *docker-resource-source - repository: concourse/registry-image-resource - tag: 1.8.0 -- name: artifactory-resource - type: registry-image - source: - <<: *docker-resource-source - repository: springio/artifactory-resource - tag: 0.0.18 -- name: github-release - type: registry-image - source: - <<: *docker-resource-source - repository: concourse/github-release-resource - tag: 1.8.0 -- name: github-status-resource - type: registry-image - source: - <<: *docker-resource-source - repository: dpb587/github-status-resource - tag: master -resources: -- name: git-repo - type: git - icon: github - source: - <<: *git-repo-resource-source -- name: ci-images-git-repo - type: git - icon: github - source: - uri: ((github-repo)) - branch: ((branch)) - paths: ["ci/images/*"] -- name: ci-image - type: registry-image - icon: docker - source: - <<: *docker-resource-source - repository: ((docker-hub-organization))/spring-framework-ci - tag: ((milestone)) -- name: artifactory-repo - type: artifactory-resource - icon: package-variant - source: - uri: ((artifactory-server)) - username: ((artifactory-username)) - password: ((artifactory-password)) - build_name: ((build-name)) -- name: github-pre-release - type: github-release - icon: briefcase-download-outline - source: - owner: spring-projects - repository: spring-framework - access_token: ((github-ci-release-token)) - pre_release: true - release: false -- name: github-release - type: github-release - icon: briefcase-download - source: - owner: spring-projects - repository: spring-framework - access_token: ((github-ci-release-token)) - pre_release: false -jobs: -- name: build-ci-images - plan: - - get: git-repo - - get: ci-images-git-repo - trigger: true - - task: build-ci-image - privileged: true - file: git-repo/ci/tasks/build-ci-image.yml - output_mapping: - image: ci-image - vars: - ci-image-name: ci-image - <<: *docker-resource-source - - put: ci-image - params: - image: ci-image/image.tar -- name: stage-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: M - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: &artifactory-params - signing_key: ((signing-key)) - signing_passphrase: ((signing-passphrase)) - repo: libs-staging-local - folder: distribution-repository - build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}" - build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}" - disable_checksum_uploads: true - threads: 8 - artifact_set: - - include: - - "/**/framework-docs-*.zip" - properties: - "zip.name": "spring-framework" - "zip.displayname": "Spring Framework" - "zip.deployed": "false" - - include: - - "/**/framework-docs-*-docs.zip" - properties: - "zip.type": "docs" - - include: - - "/**/framework-docs-*-dist.zip" - properties: - "zip.type": "dist" - - include: - - "/**/framework-docs-*-schema.zip" - properties: - "zip.type": "schema" - - put: git-repo - params: - repository: stage-git-repo -- name: promote-milestone - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-milestone] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: M - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: M - <<: *github-task-params - <<: *docker-resource-source - - put: github-pre-release - params: - <<: *changelog-task-params -- name: stage-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: RC - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - - put: git-repo - params: - repository: stage-git-repo -- name: promote-rc - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-rc] - params: - download_artifacts: false - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: RC - <<: *docker-resource-source - <<: *artifactory-task-params - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RC - <<: *github-task-params - - put: github-pre-release - params: - <<: *changelog-task-params -- name: stage-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - task: stage - image: ci-image - file: git-repo/ci/tasks/stage-version.yml - params: - RELEASE_TYPE: RELEASE - <<: *gradle-enterprise-task-params - - put: artifactory-repo - params: - <<: *artifactory-params - - put: git-repo - params: - repository: stage-git-repo -- name: promote-release - serial: true - plan: - - get: ci-image - - get: git-repo - trigger: false - - get: artifactory-repo - trigger: false - passed: [stage-release] - params: - download_artifacts: true - save_build_info: true - - task: promote - file: git-repo/ci/tasks/promote-version.yml - params: - RELEASE_TYPE: RELEASE - <<: *docker-resource-source - <<: *artifactory-task-params - <<: *sonatype-task-params -- name: create-github-release - serial: true - plan: - - get: ci-image - - get: git-repo - - get: artifactory-repo - trigger: true - passed: [promote-release] - params: - download_artifacts: false - save_build_info: true - - task: generate-changelog - file: git-repo/ci/tasks/generate-changelog.yml - params: - RELEASE_TYPE: RELEASE - <<: *docker-resource-source - <<: *github-task-params - - put: github-release - params: - <<: *changelog-task-params - -groups: -- name: "releases" - jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"] -- name: "ci-images" - jobs: ["build-ci-images"] diff --git a/ci/scripts/common.sh b/ci/scripts/common.sh deleted file mode 100644 index 1accaa616732..000000000000 --- a/ci/scripts/common.sh +++ /dev/null @@ -1,2 +0,0 @@ -source /opt/concourse-java.sh -setup_symlinks \ No newline at end of file diff --git a/ci/scripts/generate-changelog.sh b/ci/scripts/generate-changelog.sh deleted file mode 100755 index d3d2b97e5dba..000000000000 --- a/ci/scripts/generate-changelog.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e - -CONFIG_DIR=git-repo/ci/config -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) - -java -jar /github-changelog-generator.jar \ - --spring.config.location=${CONFIG_DIR}/changelog-generator.yml \ - ${version} generated-changelog/changelog.md - -echo ${version} > generated-changelog/version -echo v${version} > generated-changelog/tag diff --git a/ci/scripts/promote-version.sh b/ci/scripts/promote-version.sh deleted file mode 100755 index bd1600191a79..000000000000 --- a/ci/scripts/promote-version.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -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 /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; } - -java -jar /concourse-release-scripts.jar \ - --spring.config.location=${CONFIG_DIR}/release-scripts.yml \ - promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; } - -echo "Promotion complete" -echo $version > version/version diff --git a/ci/scripts/stage-version.sh b/ci/scripts/stage-version.sh deleted file mode 100755 index 73c57755451c..000000000000 --- a/ci/scripts/stage-version.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -set -e - -source $(dirname $0)/common.sh -repository=$(pwd)/distribution-repository - -pushd git-repo > /dev/null -git fetch --tags --all > /dev/null -popd > /dev/null - -git clone git-repo stage-git-repo > /dev/null - -pushd stage-git-repo > /dev/null - -snapshotVersion=$( awk -F '=' '$1 == "version" { print $2 }' gradle.properties ) -if [[ $RELEASE_TYPE = "M" ]]; then - stageVersion=$( get_next_milestone_release $snapshotVersion) - nextVersion=$snapshotVersion -elif [[ $RELEASE_TYPE = "RC" ]]; then - stageVersion=$( get_next_rc_release $snapshotVersion) - nextVersion=$snapshotVersion -elif [[ $RELEASE_TYPE = "RELEASE" ]]; then - stageVersion=$( get_next_release $snapshotVersion) - nextVersion=$( bump_version_number $snapshotVersion) -else - echo "Unknown release type $RELEASE_TYPE" >&2; exit 1; -fi - -echo "Staging $stageVersion (next version will be $nextVersion)" -sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties - -git config user.name "Spring Builds" > /dev/null -git config user.email "spring-builds@users.noreply.github.com" > /dev/null -git add gradle.properties > /dev/null -git commit -m"Release v$stageVersion" > /dev/null -git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null - -./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository - -git reset --hard HEAD^ > /dev/null -if [[ $nextVersion != $snapshotVersion ]]; then - echo "Setting next development version (v$nextVersion)" - sed -i "s/version=$snapshotVersion/version=$nextVersion/" gradle.properties - git add gradle.properties > /dev/null - git commit -m"Next development version (v$nextVersion)" > /dev/null -fi; - -echo "Staging Complete" - -popd > /dev/null diff --git a/ci/tasks/build-ci-image.yml b/ci/tasks/build-ci-image.yml deleted file mode 100644 index 28afb97cb629..000000000000 --- a/ci/tasks/build-ci-image.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -platform: linux -image_resource: - type: registry-image - source: - repository: concourse/oci-build-task - tag: 0.10.0 - username: ((docker-hub-username)) - password: ((docker-hub-password)) -inputs: - - name: ci-images-git-repo -outputs: - - name: image -caches: - - path: ci-image-cache -params: - CONTEXT: ci-images-git-repo/ci/images - DOCKERFILE: ci-images-git-repo/ci/images/ci-image/Dockerfile - DOCKER_HUB_AUTH: ((docker-hub-auth)) -run: - path: /bin/sh - args: - - "-c" - - | - mkdir -p /root/.docker - cat > /root/.docker/config.json < Date: Fri, 12 Jul 2024 08:57:19 +0200 Subject: [PATCH 245/261] Add missing artifact properties for staging This commit makes sure that docs artifacts have their attributes set for staging as well. Previously they were not and deployment of Javadoc did not occur. Closes gh-33207 --- .github/workflows/release.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71503639d4f4..7cdf04194e77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,14 +22,19 @@ jobs: - name: Stage Release uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 with: - build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}} - folder: 'deployment-repository' + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} password: ${{ secrets.ARTIFACTORY_PASSWORD }} + build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}} repository: 'libs-staging-local' + folder: 'deployment-repository' signing-key: ${{ secrets.GPG_PRIVATE_KEY }} signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} - uri: 'https://repo.spring.io' - username: ${{ secrets.ARTIFACTORY_USERNAME }} + artifact-properties: | + /**/framework-docs-*.zip::zip.name=spring-framework,zip.deployed=false + /**/framework-docs-*-docs.zip::zip.type=docs + /**/framework-docs-*-dist.zip::zip.type=dist + /**/framework-docs-*-schema.zip::zip.type=schema outputs: version: ${{ steps.build-and-publish.outputs.version }} verify: From 9a11c127407538cd127ea64ff2e92bfc5862bc80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Sun, 14 Jul 2024 11:10:52 +0200 Subject: [PATCH 246/261] Upgrade to Gradle 8.9 Closes gh-33215 --- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 ++++- gradlew.bat | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 8703 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 Ojj7^59?Sdh$^Qpp*~F>< delta 8662 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4413138c96c..09523c0e5490 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf13397a..f5feea6d6b11 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e4676f..9b42019c7915 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From 8da29636441ff4035679732384eaf53850d529fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 19 Jul 2024 11:47:06 +0200 Subject: [PATCH 247/261] Remove useless permissions on build-and-deploy-snapshot workflow Closes gh-33239 --- .github/workflows/build-and-deploy-snapshot.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 68147a08c9d5..eb2e0c0ece67 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -3,8 +3,6 @@ on: push: branches: - 6.0.x -permissions: - actions: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: From d33f66d9b5865d7eb76cfde70e93a25f23c5e4fb Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:51:55 +0200 Subject: [PATCH 248/261] Support single String argument for varargs invocations in SpEL Prior to this commit, the Spring Expression Language (SpEL) incorrectly split single String arguments by comma for Object... varargs method and constructor invocations. This commit addresses this by checking if the single argument type is already "assignable" to the varargs component type instead of "equal" to the varargs component type. See gh-33013 Closes gh-33189 --- .../spel/support/ReflectionHelper.java | 6 +-- .../spel/MethodInvocationTests.java | 44 +++++++++++++++++++ .../expression/spel/SpelReproTests.java | 19 +++++--- .../spel/testresources/Inventor.java | 5 +++ 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index e9b62ec2adbb..d7013b57ef0e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -298,11 +298,11 @@ static boolean convertArguments(TypeConverter converter, Object[] arguments, Exe conversionOccurred = true; } } - // If the argument type is equal to the varargs element type, there is no need to + // If the argument type is assignable to the varargs component type, there is no need to // convert it or wrap it in an array. For example, using StringToArrayConverter to // convert a String containing a comma would result in the String being split and // repackaged in an array when it should be used as-is. - else if (!sourceType.equals(targetType.getElementTypeDescriptor())) { + else if (!sourceType.isAssignableTo(targetType.getElementTypeDescriptor())) { arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType); } // Possible outcomes of the above if-else block: diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java index 7b59007195e2..c1293625d93e 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java @@ -294,6 +294,50 @@ public void testVarargsInvocation03() { evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class); } + @Test // gh-33013 + void testVarargsWithObjectArrayType() { + // Calling 'public String formatObjectVarargs(String format, Object... args)' -> String.format(format, args) + + // No var-args and no conversion necessary + evaluate("formatObjectVarargs('x')", "x", String.class); + + // No var-args but conversion necessary + evaluate("formatObjectVarargs(9)", "9", String.class); + + // No conversion necessary + evaluate("formatObjectVarargs('x -> %s', '')", "x -> ", String.class); + evaluate("formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class); + evaluate("formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class); + evaluate("formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class); + evaluate("formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class); + evaluate("formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class); + evaluate("formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class); + evaluate("formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class); + + // The following assertions were cherry-picked from 6.1.x; however, they are expected + // to fail on 6.0.x and 5.3.x, since gh-32704 (Support varargs invocations in SpEL for + // varargs array subtype) was not backported. + // evaluate("formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class); + // evaluate("formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class); + // evaluate("formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class); + // evaluate("formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class); + + // Conversion necessary + evaluate("formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class); + evaluate("formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class); + + // Individual string contains a comma with multiple varargs arguments + evaluate("formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class); + evaluate("formatObjectVarargs('foo -> %s %s', 'bar', ',baz')", "foo -> bar ,baz", String.class); + evaluate("formatObjectVarargs('foo -> %s %s', 'bar,', 'baz')", "foo -> bar, baz", String.class); + + // Individual string contains a comma with single varargs argument. + evaluate("formatObjectVarargs('foo -> %s', ',')", "foo -> ,", String.class); + evaluate("formatObjectVarargs('foo -> %s', ',bar')", "foo -> ,bar", String.class); + evaluate("formatObjectVarargs('foo -> %s', 'bar,')", "foo -> bar,", String.class); + evaluate("formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class); + } + @Test public void testVarargsOptionalInvocation() { // Calling 'public String optionalVarargsMethod(Optional... values)' diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java index 00d9581f3818..f7b9c382bb3e 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -66,6 +66,7 @@ import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.InstanceOfAssertFactories.list; /** * Reproduction tests cornering various reported SpEL issues. @@ -1442,14 +1443,20 @@ void SPR12502() { assertThat(expression.getValue(new NamedUser())).isEqualTo(NamedUser.class.getName()); } - @Test - @SuppressWarnings("rawtypes") - void SPR12522() { + @Test // gh-17127, SPR-12522 + void arraysAsListWithNoArguments() { + SpelExpressionParser parser = new SpelExpressionParser(); + Expression expression = parser.parseExpression("T(java.util.Arrays).asList()"); + List value = expression.getValue(List.class); + assertThat(value).isEmpty(); + } + + @Test // gh-33013 + void arraysAsListWithSingleEmptyStringArgument() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("T(java.util.Arrays).asList('')"); - Object value = expression.getValue(); - assertThat(value).isInstanceOf(List.class); - assertThat(((List) value).isEmpty()).isTrue(); + List value = expression.getValue(List.class); + assertThat(value).asInstanceOf(list(String.class)).containsExactly(""); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java index 282622cf7d2d..3979d919e096 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java @@ -213,6 +213,11 @@ public String aVarargsMethod3(String str1, String... strings) { return str1 + "-" + String.join("-", strings); } + public String formatObjectVarargs(String format, Object... args) { + return String.format(format, args); + } + + public Inventor(String... strings) { } From e11a419cc32a5c680070d7e7199e5c2a833cb94c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 4 Aug 2024 16:42:38 +0300 Subject: [PATCH 249/261] Revert "Support single String argument for varargs invocations in SpEL" This reverts commit d33f66d9b5865d7eb76cfde70e93a25f23c5e4fb. See gh-33013 See gh-33189 --- .../spel/support/ReflectionHelper.java | 6 +-- .../spel/MethodInvocationTests.java | 44 ------------------- .../expression/spel/SpelReproTests.java | 19 +++----- .../spel/testresources/Inventor.java | 5 --- 4 files changed, 9 insertions(+), 65 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index d7013b57ef0e..e9b62ec2adbb 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2021 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. @@ -298,11 +298,11 @@ static boolean convertArguments(TypeConverter converter, Object[] arguments, Exe conversionOccurred = true; } } - // If the argument type is assignable to the varargs component type, there is no need to + // If the argument type is equal to the varargs element type, there is no need to // convert it or wrap it in an array. For example, using StringToArrayConverter to // convert a String containing a comma would result in the String being split and // repackaged in an array when it should be used as-is. - else if (!sourceType.isAssignableTo(targetType.getElementTypeDescriptor())) { + else if (!sourceType.equals(targetType.getElementTypeDescriptor())) { arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType); } // Possible outcomes of the above if-else block: diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java index c1293625d93e..7b59007195e2 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java @@ -294,50 +294,6 @@ public void testVarargsInvocation03() { evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class); } - @Test // gh-33013 - void testVarargsWithObjectArrayType() { - // Calling 'public String formatObjectVarargs(String format, Object... args)' -> String.format(format, args) - - // No var-args and no conversion necessary - evaluate("formatObjectVarargs('x')", "x", String.class); - - // No var-args but conversion necessary - evaluate("formatObjectVarargs(9)", "9", String.class); - - // No conversion necessary - evaluate("formatObjectVarargs('x -> %s', '')", "x -> ", String.class); - evaluate("formatObjectVarargs('x -> %s', ' ')", "x -> ", String.class); - evaluate("formatObjectVarargs('x -> %s', 'a')", "x -> a", String.class); - evaluate("formatObjectVarargs('x -> %s %s %s', 'a', 'b', 'c')", "x -> a b c", String.class); - evaluate("formatObjectVarargs('x -> %s', new Object[]{''})", "x -> ", String.class); - evaluate("formatObjectVarargs('x -> %s', new Object[]{' '})", "x -> ", String.class); - evaluate("formatObjectVarargs('x -> %s', new Object[]{'a'})", "x -> a", String.class); - evaluate("formatObjectVarargs('x -> %s %s %s', new Object[]{'a', 'b', 'c'})", "x -> a b c", String.class); - - // The following assertions were cherry-picked from 6.1.x; however, they are expected - // to fail on 6.0.x and 5.3.x, since gh-32704 (Support varargs invocations in SpEL for - // varargs array subtype) was not backported. - // evaluate("formatObjectVarargs('x -> %s', new String[]{''})", "x -> ", String.class); - // evaluate("formatObjectVarargs('x -> %s', new String[]{' '})", "x -> ", String.class); - // evaluate("formatObjectVarargs('x -> %s', new String[]{'a'})", "x -> a", String.class); - // evaluate("formatObjectVarargs('x -> %s %s %s', new String[]{'a', 'b', 'c'})", "x -> a b c", String.class); - - // Conversion necessary - evaluate("formatObjectVarargs('x -> %s %s', 2, 3)", "x -> 2 3", String.class); - evaluate("formatObjectVarargs('x -> %s %s', 'a', 3.0d)", "x -> a 3.0", String.class); - - // Individual string contains a comma with multiple varargs arguments - evaluate("formatObjectVarargs('foo -> %s %s', ',', 'baz')", "foo -> , baz", String.class); - evaluate("formatObjectVarargs('foo -> %s %s', 'bar', ',baz')", "foo -> bar ,baz", String.class); - evaluate("formatObjectVarargs('foo -> %s %s', 'bar,', 'baz')", "foo -> bar, baz", String.class); - - // Individual string contains a comma with single varargs argument. - evaluate("formatObjectVarargs('foo -> %s', ',')", "foo -> ,", String.class); - evaluate("formatObjectVarargs('foo -> %s', ',bar')", "foo -> ,bar", String.class); - evaluate("formatObjectVarargs('foo -> %s', 'bar,')", "foo -> bar,", String.class); - evaluate("formatObjectVarargs('foo -> %s', 'bar,baz')", "foo -> bar,baz", String.class); - } - @Test public void testVarargsOptionalInvocation() { // Calling 'public String optionalVarargsMethod(Optional... values)' diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java index f7b9c382bb3e..00d9581f3818 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -66,7 +66,6 @@ import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.InstanceOfAssertFactories.list; /** * Reproduction tests cornering various reported SpEL issues. @@ -1443,20 +1442,14 @@ void SPR12502() { assertThat(expression.getValue(new NamedUser())).isEqualTo(NamedUser.class.getName()); } - @Test // gh-17127, SPR-12522 - void arraysAsListWithNoArguments() { - SpelExpressionParser parser = new SpelExpressionParser(); - Expression expression = parser.parseExpression("T(java.util.Arrays).asList()"); - List value = expression.getValue(List.class); - assertThat(value).isEmpty(); - } - - @Test // gh-33013 - void arraysAsListWithSingleEmptyStringArgument() { + @Test + @SuppressWarnings("rawtypes") + void SPR12522() { SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("T(java.util.Arrays).asList('')"); - List value = expression.getValue(List.class); - assertThat(value).asInstanceOf(list(String.class)).containsExactly(""); + Object value = expression.getValue(); + assertThat(value).isInstanceOf(List.class); + assertThat(((List) value).isEmpty()).isTrue(); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java index 3979d919e096..282622cf7d2d 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Inventor.java @@ -213,11 +213,6 @@ public String aVarargsMethod3(String str1, String... strings) { return str1 + "-" + String.join("-", strings); } - public String formatObjectVarargs(String format, Object... args) { - return String.format(format, args); - } - - public Inventor(String... strings) { } From 3e7372491c6a152c473c746dd75e029e0a6ac8b8 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 4 Aug 2024 17:13:56 +0300 Subject: [PATCH 250/261] Support conversion from primitive array to Object[] in ConversionService Prior to this commit, the ConversionService failed to convert a primitive array (such as int[]) to an Object[] due to an error in the logic in ArrayToArrayConverter. This commit addresses this by augmenting the "can bypass conversion" check in ArrayToArrayConverter to ensure that the supplied source object is an instance of the target type (i.e., that the source array can be cast to the target type array without conversion). Closes gh-33212 (cherry picked from commit cb6a5baac508921486fd13a7a91cab9d7625868b) --- .../support/ArrayToArrayConverter.java | 7 +++--- .../DefaultConversionServiceTests.java | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java index 7c345d1c76d9..10307745227a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -34,6 +34,7 @@ * * @author Keith Donald * @author Phillip Webb + * @author Sam Brannen * @since 3.0 */ final class ArrayToArrayConverter implements ConditionalGenericConverter { @@ -64,8 +65,8 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (this.conversionService instanceof GenericConversionService genericConversionService) { TypeDescriptor targetElement = targetType.getElementTypeDescriptor(); - if (targetElement != null && genericConversionService.canBypassConvert( - sourceType.getElementTypeDescriptor(), targetElement)) { + if (targetElement != null && targetType.getType().isInstance(source) && + genericConversionService.canBypassConvert(sourceType.getElementTypeDescriptor(), targetElement)) { return source; } } diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index 1709a9a2032d..a64c2e48f1ab 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -607,8 +607,26 @@ void convertObjectArrayToIntArray() { assertThat(result).containsExactly(1, 2, 3); } + @Test // gh-33212 + void convertIntArrayToObjectArray() { + Object[] result = conversionService.convert(new int[] {1, 2}, Object[].class); + assertThat(result).containsExactly(1, 2); + } + + @Test + void convertIntArrayToFloatArray() { + Float[] result = conversionService.convert(new int[] {1, 2}, Float[].class); + assertThat(result).containsExactly(1.0F, 2.0F); + } + + @Test + void convertIntArrayToPrimitiveFloatArray() { + float[] result = conversionService.convert(new int[] {1, 2}, float[].class); + assertThat(result).containsExactly(1.0F, 2.0F); + } + @Test - void convertByteArrayToWrapperArray() { + void convertPrimitiveByteArrayToByteWrapperArray() { byte[] byteArray = {1, 2, 3}; Byte[] converted = conversionService.convert(byteArray, Byte[].class); assertThat(converted).isEqualTo(new Byte[]{1, 2, 3}); From bf72e921458cfb3a0f06763aeb3a37754f57c982 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:15:39 +0300 Subject: [PATCH 251/261] Polishing --- .../expression/spel/PropertyAccessTests.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java index 6d7d669217db..1117882ae561 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import org.assertj.core.api.ThrowableTypeAssert; import org.junit.jupiter.api.Test; import org.springframework.core.convert.TypeDescriptor; @@ -83,11 +84,11 @@ void nonExistentPropertiesAndMethods() { void accessingOnNullObject() { SpelExpression expr = (SpelExpression) parser.parseExpression("madeup"); EvaluationContext context = new StandardEvaluationContext(null); - assertThatExceptionOfType(SpelEvaluationException.class) + assertThatSpelEvaluationException() .isThrownBy(() -> expr.getValue(context)) .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL); assertThat(expr.isWritable(context)).isFalse(); - assertThatExceptionOfType(SpelEvaluationException.class) + assertThatSpelEvaluationException() .isThrownBy(() -> expr.setValue(context, "abc")) .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL); } @@ -117,8 +118,7 @@ void addingSpecificPropertyAccessor() { assertThat((int) i).isEqualTo(99); // Cannot set it to a string value - assertThatExceptionOfType(EvaluationException.class).isThrownBy(() -> - flibbleexpr.setValue(ctx, "not allowed")); + assertThatSpelEvaluationException().isThrownBy(() -> flibbleexpr.setValue(ctx, "not allowed")); // message will be: EL1063E:(pos 20): A problem occurred whilst attempting to set the property // 'flibbles': 'Cannot set flibbles to an object of type 'class java.lang.String'' // System.out.println(e.getMessage()); @@ -173,8 +173,7 @@ void standardGetClassAccess() { @Test void noGetClassAccess() { EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("'a'.class.name").getValue(context)); + assertThatSpelEvaluationException().isThrownBy(() -> parser.parseExpression("'a'.class.name").getValue(context)); } @Test @@ -187,8 +186,9 @@ void propertyReadOnly() { target.setName("p2"); assertThat(expr.getValue(context, target)).isEqualTo("p2"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("name='p3'").getValue(context, target)); + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target)) + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE); } @Test @@ -201,8 +201,9 @@ void propertyReadOnlyWithRecordStyle() { RecordPerson target2 = new RecordPerson("p2"); assertThat(expr.getValue(context, target2)).isEqualTo("p2"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> - parser.parseExpression("name='p3'").getValue(context, target2)); + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target2)) + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE); } @Test @@ -248,7 +249,7 @@ void propertyReadWriteWithRootObject() { void propertyAccessWithoutMethodResolver() { EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); Person target = new Person("p1"); - assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> + assertThatSpelEvaluationException().isThrownBy(() -> parser.parseExpression("name.substring(1)").getValue(context, target)); } @@ -274,12 +275,17 @@ void propertyAccessWithInstanceMethodResolverAndTypedRootObject() { void propertyAccessWithArrayIndexOutOfBounds() { EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); Expression expression = parser.parseExpression("stringArrayOfThreeItems[3]"); - assertThatExceptionOfType(SpelEvaluationException.class) + assertThatSpelEvaluationException() .isThrownBy(() -> expression.getValue(context, new Inventor())) .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS); } + private ThrowableTypeAssert assertThatSpelEvaluationException() { + return assertThatExceptionOfType(SpelEvaluationException.class); + } + + // This can resolve the property 'flibbles' on any String (very useful...) private static class StringyPropertyAccessor implements PropertyAccessor { From 2165b1651bf29fcae27f0b8a134e688e34e9554e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 5 Aug 2024 14:15:55 +0300 Subject: [PATCH 252/261] Throw exception for failure to set property as index in SpEL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, the Indexer in the Spring Expression Language (SpEL) silently ignored a failure to set a property via the indexed property syntax ([''] = ) – for example, if property write access was disabled in the EvaluationContext. This commit addresses this issue by properly throwing a SpelEvaluationException in PropertyIndexingValueRef.setValue(Object) if the property could not be set. See gh-33310 Closes gh-33311 (cherry picked from commit c57c2272a1d2a214bbbacac166a0845f287bbc73) --- .../java/org/springframework/expression/spel/ast/Indexer.java | 2 ++ .../springframework/expression/spel/PropertyAccessTests.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 9dc12f91fdd0..a56199ad33b5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -630,6 +630,8 @@ public void setValue(@Nullable Object newValue) { throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, this.name, ex.getMessage()); } + throw new SpelEvaluationException(getStartPosition(), + SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString()); } @Override diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java index 1117882ae561..69f67c32528e 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java @@ -189,6 +189,10 @@ void propertyReadOnly() { assertThatSpelEvaluationException() .isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target)) .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE); + + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("['name']='p4'").getValue(context, target)) + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE); } @Test From e1ab306506d4e350b119331387f504b726fbc776 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:33:03 +0300 Subject: [PATCH 253/261] Enforce read-only semantics in SpEL's SimpleEvaluationContext SimpleEvaluationContext.forReadOnlyDataBinding() documents that it creates a SimpleEvaluationContext for read-only access to public properties; however, prior to this commit write access was not disabled for indexed structures when using the assignment operator, the increment operator, or the decrement operator. In order to better align with the documented contract for forReadOnlyDataBinding(), this commit makes it possible to disable assignment in general in order to enforce read-only semantics for SpEL's SimpleEvaluationContext when created via the forReadOnlyDataBinding() factory method. Specifically: - This commit introduces a new isAssignmentEnabled() "default" method in the EvaluationContext API, which returns true by default. - SimpleEvaluationContext overrides isAssignmentEnabled(), returning false if the context was created via the forReadOnlyDataBinding() factory method. - The Assign, OpDec, and OpInc AST nodes -- representing the assignment (=), increment (++), and decrement (--) operators, respectively -- now throw a SpelEvaluationException if assignment is disabled for the current EvaluationContext. See gh-33319 Closes gh-33321 (cherry picked from commit 0127de5a7a80319a9a7973ad125002309a591a77) --- .../expression/EvaluationContext.java | 16 +- .../expression/spel/ast/Assign.java | 7 +- .../expression/spel/ast/OpDec.java | 6 +- .../expression/spel/ast/OpInc.java | 8 +- .../spel/support/SimpleEvaluationContext.java | 77 ++- .../spel/CompilableMapAccessor.java | 117 +++++ .../expression/spel/PropertyAccessTests.java | 8 +- .../spel/SpelCompilationCoverageTests.java | 78 --- .../support/SimpleEvaluationContextTests.java | 477 ++++++++++++++++++ 9 files changed, 685 insertions(+), 109 deletions(-) create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java index 0c393a86dbe6..5bb6c46e0d65 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,4 +132,18 @@ default TypedValue assignVariable(String name, Supplier valueSupplie @Nullable Object lookupVariable(String name); + /** + * Determine if assignment is enabled within expressions evaluated by this evaluation + * context. + *

      If this method returns {@code false}, the assignment ({@code =}), increment + * ({@code ++}), and decrement ({@code --}) operators are disabled. + *

      By default, this method returns {@code true}. Concrete implementations may override + * this default method to disable assignment. + * @return {@code true} if assignment is enabled; {@code false} otherwise + * @since 5.3.38 + */ + default boolean isAssignmentEnabled() { + return true; + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java index 55e5d2e4ff08..1b47ead1607f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Assign.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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,8 @@ import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; /** * Represents assignment. An alternative to calling {@code setValue} @@ -39,6 +41,9 @@ public Assign(int startPos, int endPos, SpelNodeImpl... operands) { @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.NOT_ASSIGNABLE, toStringAST()); + } return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state)); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java index d61e8d641062..ce15fdc6072b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpDec.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -51,6 +51,10 @@ public OpDec(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_DECREMENTABLE, toStringAST()); + } + SpelNodeImpl operand = getLeftOperand(); // The operand is going to be read and then assigned to, we don't want to evaluate it twice. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java index f6dc184c0f5e..59640a1858a8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpInc.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -51,6 +51,10 @@ public OpInc(int startPos, int endPos, boolean postfix, SpelNodeImpl... operands @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + if (!state.getEvaluationContext().isAssignmentEnabled()) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.OPERAND_NOT_INCREMENTABLE, toStringAST()); + } + SpelNodeImpl operand = getLeftOperand(); ValueRef valueRef = operand.getValueRef(state); @@ -104,7 +108,7 @@ else if (op1 instanceof Byte) { } } - // set the name value + // set the new value try { valueRef.setValue(newValue.getValue()); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index 1168c9c91a26..8e0f11be50f4 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -51,25 +51,25 @@ * SpEL language syntax, e.g. excluding references to Java types, constructors, * and bean references. * - *

      When creating a {@code SimpleEvaluationContext} you need to choose the - * level of support that you need for property access in SpEL expressions: + *

      When creating a {@code SimpleEvaluationContext} you need to choose the level of + * support that you need for data binding in SpEL expressions: *

        - *
      • A custom {@code PropertyAccessor} (typically not reflection-based), - * potentially combined with a {@link DataBindingPropertyAccessor}
      • - *
      • Data binding properties for read-only access
      • - *
      • Data binding properties for read and write
      • + *
      • Data binding for read-only access
      • + *
      • Data binding for read and write access
      • + *
      • A custom {@code PropertyAccessor} (typically not reflection-based), potentially + * combined with a {@link DataBindingPropertyAccessor}
      • *
      * - *

      Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} - * enables read access to properties via {@link DataBindingPropertyAccessor}; - * same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when - * write access is needed as well. Alternatively, configure custom accessors - * via {@link SimpleEvaluationContext#forPropertyAccessors}, and potentially - * activate method resolution and/or a type converter through the builder. + *

      Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()} enables + * read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly, + * {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access + * to properties. Alternatively, configure custom accessors via + * {@link SimpleEvaluationContext#forPropertyAccessors} and potentially activate method + * resolution and/or a type converter through the builder. * *

      Note that {@code SimpleEvaluationContext} is typically not configured * with a default root object. Instead it is meant to be created once and - * used repeatedly through {@code getValue} calls on a pre-compiled + * used repeatedly through {@code getValue} calls on a predefined * {@link org.springframework.expression.Expression} with both an * {@code EvaluationContext} and a root object as arguments: * {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}. @@ -81,9 +81,9 @@ * @author Juergen Hoeller * @author Sam Brannen * @since 4.3.15 - * @see #forPropertyAccessors * @see #forReadOnlyDataBinding() * @see #forReadWriteDataBinding() + * @see #forPropertyAccessors * @see StandardEvaluationContext * @see StandardTypeConverter * @see DataBindingPropertyAccessor @@ -109,14 +109,17 @@ public final class SimpleEvaluationContext implements EvaluationContext { private final Map variables = new HashMap<>(); + private final boolean assignmentEnabled; + private SimpleEvaluationContext(List accessors, List resolvers, - @Nullable TypeConverter converter, @Nullable TypedValue rootObject) { + @Nullable TypeConverter converter, @Nullable TypedValue rootObject, boolean assignmentEnabled) { this.propertyAccessors = accessors; this.methodResolvers = resolvers; this.typeConverter = (converter != null ? converter : new StandardTypeConverter()); this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL); + this.assignmentEnabled = assignmentEnabled; } @@ -224,15 +227,33 @@ public Object lookupVariable(String name) { return this.variables.get(name); } + /** + * Determine if assignment is enabled within expressions evaluated by this evaluation + * context. + *

      If this method returns {@code false}, the assignment ({@code =}), increment + * ({@code ++}), and decrement ({@code --}) operators are disabled. + * @return {@code true} if assignment is enabled; {@code false} otherwise + * @since 5.3.38 + * @see #forPropertyAccessors(PropertyAccessor...) + * @see #forReadOnlyDataBinding() + * @see #forReadWriteDataBinding() + */ + @Override + public boolean isAssignmentEnabled() { + return this.assignmentEnabled; + } /** * Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor} * delegates: typically a custom {@code PropertyAccessor} specific to a use case * (e.g. attribute resolution in a custom data structure), potentially combined with * a {@link DataBindingPropertyAccessor} if property dereferences are needed as well. + *

      Assignment is enabled within expressions evaluated by the context created via + * this factory method. * @param accessors the accessor delegates to use * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see DataBindingPropertyAccessor#forReadWriteAccess() + * @see #isAssignmentEnabled() */ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { for (PropertyAccessor accessor : accessors) { @@ -241,34 +262,40 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { "ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass."); } } - return new Builder(accessors); + return new Builder(true, accessors); } /** * Create a {@code SimpleEvaluationContext} for read-only access to * public properties via {@link DataBindingPropertyAccessor}. + *

      Assignment is disabled within expressions evaluated by the context created via + * this factory method. * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see #forPropertyAccessors + * @see #isAssignmentEnabled() */ public static Builder forReadOnlyDataBinding() { - return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()); + return new Builder(false, DataBindingPropertyAccessor.forReadOnlyAccess()); } /** * Create a {@code SimpleEvaluationContext} for read-write access to * public properties via {@link DataBindingPropertyAccessor}. + *

      Assignment is enabled within expressions evaluated by the context created via + * this factory method. * @see DataBindingPropertyAccessor#forReadWriteAccess() * @see #forPropertyAccessors + * @see #isAssignmentEnabled() */ public static Builder forReadWriteDataBinding() { - return new Builder(DataBindingPropertyAccessor.forReadWriteAccess()); + return new Builder(true, DataBindingPropertyAccessor.forReadWriteAccess()); } /** * Builder for {@code SimpleEvaluationContext}. */ - public static class Builder { + public static final class Builder { private final List accessors; @@ -280,10 +307,15 @@ public static class Builder { @Nullable private TypedValue rootObject; - public Builder(PropertyAccessor... accessors) { + private final boolean assignmentEnabled; + + + private Builder(boolean assignmentEnabled, PropertyAccessor... accessors) { + this.assignmentEnabled = assignmentEnabled; this.accessors = Arrays.asList(accessors); } + /** * Register the specified {@link MethodResolver} delegates for * a combination of property access and method resolution. @@ -362,7 +394,8 @@ public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescrip } public SimpleEvaluationContext build() { - return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject); + return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject, + this.assignmentEnabled); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java new file mode 100644 index 000000000000..0d065f5bb298 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2024 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.expression.spel; + +import java.util.Map; + +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.AccessException; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.TypedValue; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * This is a local COPY of {@link org.springframework.context.expression.MapAccessor}. + * + * @author Juergen Hoeller + * @author Andy Clement + * @since 4.1 + */ +public class CompilableMapAccessor implements CompilablePropertyAccessor { + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + @Override + public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return (target instanceof Map map && map.containsKey(name)); + } + + @Override + public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + Assert.state(target instanceof Map, "Target must be of type Map"); + Map map = (Map) target; + Object value = map.get(name); + if (value == null && !map.containsKey(name)) { + throw new MapAccessException(name); + } + return new TypedValue(value); + } + + @Override + public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) + throws AccessException { + + Assert.state(target instanceof Map, "Target must be a Map"); + Map map = (Map) target; + map.put(name, newValue); + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + @Override + public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { + String descriptor = cf.lastDescriptor(); + if (descriptor == null || !descriptor.equals("Ljava/util/Map")) { + if (descriptor == null) { + cf.loadTarget(mv); + } + CodeFlow.insertCheckCast(mv, "Ljava/util/Map"); + } + mv.visitLdcInsn(propertyName); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); + } + + + /** + * Exception thrown from {@code read} in order to reset a cached + * PropertyAccessor, allowing other accessors to have a try. + */ + @SuppressWarnings("serial") + private static class MapAccessException extends AccessException { + + private final String key; + + public MapAccessException(String key) { + super(""); + this.key = key; + } + + @Override + public String getMessage() { + return "Map does not contain a value for key '" + this.key + "'"; + } + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java index 69f67c32528e..680c0dd63c8c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -188,11 +188,11 @@ void propertyReadOnly() { assertThatSpelEvaluationException() .isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target)) - .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE); + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE); assertThatSpelEvaluationException() .isThrownBy(() -> parser.parseExpression("['name']='p4'").getValue(context, target)) - .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE); + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE); } @Test @@ -207,7 +207,7 @@ void propertyReadOnlyWithRecordStyle() { assertThatSpelEvaluationException() .isThrownBy(() -> parser.parseExpression("name='p3'").getValue(context, target2)) - .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE); + .extracting(SpelEvaluationException::getMessageCode).isEqualTo(SpelMessage.NOT_ASSIGNABLE); } @Test diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 6667cc0cc0ce..bf5ae88dbe6b 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -5946,84 +5946,6 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { } - static class CompilableMapAccessor implements CompilablePropertyAccessor { - - @Override - public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { - Map map = (Map) target; - return map.containsKey(name); - } - - @Override - public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { - Map map = (Map) target; - Object value = map.get(name); - if (value == null && !map.containsKey(name)) { - throw new MapAccessException(name); - } - return new TypedValue(value); - } - - @Override - public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { - return true; - } - - @Override - @SuppressWarnings("unchecked") - public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { - Map map = (Map) target; - map.put(name, newValue); - } - - @Override - public Class[] getSpecificTargetClasses() { - return new Class[] {Map.class}; - } - - @Override - public boolean isCompilable() { - return true; - } - - @Override - public Class getPropertyType() { - return Object.class; - } - - @Override - public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { - String descriptor = cf.lastDescriptor(); - if (descriptor == null) { - cf.loadTarget(mv); - } - mv.visitLdcInsn(propertyName); - mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); - } - } - - - /** - * Exception thrown from {@code read} in order to reset a cached - * PropertyAccessor, allowing other accessors to have a try. - */ - @SuppressWarnings("serial") - private static class MapAccessException extends AccessException { - - private final String key; - - public MapAccessException(String key) { - super(null); - this.key = key; - } - - @Override - public String getMessage() { - return "Map does not contain a value for key '" + this.key + "'"; - } - } - - public static class Greeter { public String getWorld() { diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java new file mode 100644 index 000000000000..7ac2132883c3 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java @@ -0,0 +1,477 @@ +/* + * Copyright 2002-2024 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.expression.spel.support; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.ThrowableTypeAssert; +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.CompilableMapAccessor; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link SimpleEvaluationContext}. + * + *

      Some of the use cases in this test class are duplicated elsewhere within the test + * suite; however, we include them here to consistently focus on related features in this + * test class. + * + * @author Sam Brannen + */ +class SimpleEvaluationContextTests { + + private final SpelExpressionParser parser = new SpelExpressionParser(); + + private final Model model = new Model(); + + + @Test + void forReadWriteDataBinding() { + SimpleEvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + + assertReadWriteMode(context); + } + + @Test + void forReadOnlyDataBinding() { + SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + + assertCommonReadOnlyModeBehavior(context); + + // WRITE -- via assignment operator + + // Variable + assertAssignmentDisabled(context, "#myVar = 'rejected'"); + + // Property + assertAssignmentDisabled(context, "name = 'rejected'"); + assertIncrementDisabled(context, "count++"); + assertIncrementDisabled(context, "++count"); + assertDecrementDisabled(context, "count--"); + assertDecrementDisabled(context, "--count"); + + // Array Index + assertAssignmentDisabled(context, "array[0] = 'rejected'"); + assertIncrementDisabled(context, "numbers[0]++"); + assertIncrementDisabled(context, "++numbers[0]"); + assertDecrementDisabled(context, "numbers[0]--"); + assertDecrementDisabled(context, "--numbers[0]"); + + // List Index + assertAssignmentDisabled(context, "list[0] = 'rejected'"); + + // Map Index -- key as String + assertAssignmentDisabled(context, "map['red'] = 'rejected'"); + + // Map Index -- key as pseudo property name + assertAssignmentDisabled(context, "map[yellow] = 'rejected'"); + + // String Index + assertAssignmentDisabled(context, "name[0] = 'rejected'"); + + // Object Index + assertAssignmentDisabled(context, "['name'] = 'rejected'"); + } + + @Test + void forPropertyAccessorsInReadWriteMode() { + SimpleEvaluationContext context = SimpleEvaluationContext + .forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadWriteAccess()) + .build(); + + assertReadWriteMode(context); + + // Map -- with key as property name supported by CompilableMapAccessor + + Expression expression; + expression = parser.parseExpression("map.yellow"); + expression.setValue(context, model, "pineapple"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple"); + + expression = parser.parseExpression("map.yellow = 'banana'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + expression = parser.parseExpression("map.yellow"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + } + + /** + * We call this "mixed" read-only mode, because write access via PropertyAccessors is + * disabled, but write access via the Indexer is not disabled. + */ + @Test + void forPropertyAccessorsInMixedReadOnlyMode() { + SimpleEvaluationContext context = SimpleEvaluationContext + .forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess()) + .build(); + + assertCommonReadOnlyModeBehavior(context); + + // Map -- with key as property name supported by CompilableMapAccessor + + Expression expression; + expression = parser.parseExpression("map.yellow"); + expression.setValue(context, model, "pineapple"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple"); + + expression = parser.parseExpression("map.yellow = 'banana'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + expression = parser.parseExpression("map.yellow"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + + // WRITE -- via assignment operator + + // Variable + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("#myVar = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED)); + + // Property + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE)); + + // Array Index + parser.parseExpression("array[0]").setValue(context, model, "foo"); + assertThat(model.array).containsExactly("foo"); + + // List Index + parser.parseExpression("list[0]").setValue(context, model, "cat"); + assertThat(model.list).containsExactly("cat"); + + // Map Index -- key as String + parser.parseExpression("map['red']").setValue(context, model, "cherry"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "banana")); + + // Map Index -- key as pseudo property name + parser.parseExpression("map[yellow]").setValue(context, model, "lemon"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon")); + + // String Index + // The Indexer does not support writes when indexing into a String. + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name[0] = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE)); + + // Object Index + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("['name'] = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE)); + + // WRITE -- via increment and decrement operators + + assertIncrementAndDecrementWritesForIndexedStructures(context); + } + + + private void assertReadWriteMode(SimpleEvaluationContext context) { + // Variables can always be set programmatically within an EvaluationContext. + context.setVariable("myVar", "enigma"); + + // WRITE -- via setValue() + + // Property + parser.parseExpression("name").setValue(context, model, "test"); + assertThat(model.name).isEqualTo("test"); + parser.parseExpression("count").setValue(context, model, 42); + assertThat(model.count).isEqualTo(42); + + // Array Index + parser.parseExpression("array[0]").setValue(context, model, "foo"); + assertThat(model.array).containsExactly("foo"); + + // List Index + parser.parseExpression("list[0]").setValue(context, model, "cat"); + assertThat(model.list).containsExactly("cat"); + + // Map Index -- key as String + parser.parseExpression("map['red']").setValue(context, model, "cherry"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "replace me")); + + // Map Index -- key as pseudo property name + parser.parseExpression("map[yellow]").setValue(context, model, "lemon"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon")); + + // READ + assertReadAccess(context); + + // WRITE -- via assignment operator + + // Variable assignment is always disabled in a SimpleEvaluationContext. + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("#myVar = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED)); + + Expression expression; + + // Property + expression = parser.parseExpression("name = 'changed'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("changed"); + expression = parser.parseExpression("name"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("changed"); + + // Array Index + expression = parser.parseExpression("array[0] = 'bar'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("bar"); + expression = parser.parseExpression("array[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("bar"); + + // List Index + expression = parser.parseExpression("list[0] = 'dog'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("dog"); + expression = parser.parseExpression("list[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("dog"); + + // Map Index -- key as String + expression = parser.parseExpression("map['red'] = 'strawberry'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("strawberry"); + expression = parser.parseExpression("map['red']"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("strawberry"); + + // Map Index -- key as pseudo property name + expression = parser.parseExpression("map[yellow] = 'banana'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + expression = parser.parseExpression("map[yellow]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("banana"); + + // String Index + // The Indexer does not support writes when indexing into a String. + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name[0] = 'rejected'").getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE)); + + // Object Index + expression = parser.parseExpression("['name'] = 'new name'"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("new name"); + expression = parser.parseExpression("['name']"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("new name"); + + // WRITE -- via increment and decrement operators + + assertIncrementAndDecrementWritesForProperties(context); + assertIncrementAndDecrementWritesForIndexedStructures(context); + } + + private void assertCommonReadOnlyModeBehavior(SimpleEvaluationContext context) { + // Variables can always be set programmatically within an EvaluationContext. + context.setVariable("myVar", "enigma"); + + // WRITE -- via setValue() + + // Note: forReadOnlyDataBinding() disables programmatic writes via setValue() for + // properties but allows programmatic writes via setValue() for indexed structures. + + // Property + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("name").setValue(context, model, "test")) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE)); + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression("count").setValue(context, model, 42)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE)); + + // Array Index + parser.parseExpression("array[0]").setValue(context, model, "foo"); + assertThat(model.array).containsExactly("foo"); + + // List Index + parser.parseExpression("list[0]").setValue(context, model, "cat"); + assertThat(model.list).containsExactly("cat"); + + // Map Index -- key as String + parser.parseExpression("map['red']").setValue(context, model, "cherry"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "replace me")); + + // Map Index -- key as pseudo property name + parser.parseExpression("map[yellow]").setValue(context, model, "lemon"); + assertThat(model.map).containsOnly(entry("red", "cherry"), entry("yellow", "lemon")); + + // Since the setValue() attempts for "name" and "count" failed above, we have to set + // them directly for assertReadAccess(). + model.name = "test"; + model.count = 42; + + // READ + assertReadAccess(context); + } + + private void assertReadAccess(SimpleEvaluationContext context) { + Expression expression; + + // Variable + expression = parser.parseExpression("#myVar"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("enigma"); + + // Property + expression = parser.parseExpression("name"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("test"); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42); + + // Array Index + expression = parser.parseExpression("array[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("foo"); + + // List Index + expression = parser.parseExpression("list[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("cat"); + + // Map Index -- key as String + expression = parser.parseExpression("map['red']"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("cherry"); + + // Map Index -- key as pseudo property name + expression = parser.parseExpression("map[yellow]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("lemon"); + + // String Index + expression = parser.parseExpression("name[0]"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("t"); + + // Object Index + expression = parser.parseExpression("['name']"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("test"); + } + + private void assertIncrementAndDecrementWritesForProperties(SimpleEvaluationContext context) { + Expression expression; + expression = parser.parseExpression("count++"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(43); + + expression = parser.parseExpression("++count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44); + + expression = parser.parseExpression("count--"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(44); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(43); + + expression = parser.parseExpression("--count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42); + expression = parser.parseExpression("count"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(42); + } + + private void assertIncrementAndDecrementWritesForIndexedStructures(SimpleEvaluationContext context) { + Expression expression; + expression = parser.parseExpression("numbers[0]++"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99); + expression = parser.parseExpression("numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(100); + + expression = parser.parseExpression("++numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101); + expression = parser.parseExpression("numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101); + + expression = parser.parseExpression("numbers[0]--"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(101); + expression = parser.parseExpression("numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(100); + + expression = parser.parseExpression("--numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99); + expression = parser.parseExpression("numbers[0]"); + assertThat(expression.getValue(context, model, Integer.class)).isEqualTo(99); + } + + private ThrowableTypeAssert assertThatSpelEvaluationException() { + return assertThatExceptionOfType(SpelEvaluationException.class); + } + + private void assertAssignmentDisabled(SimpleEvaluationContext context, String expression) { + assertEvaluationException(context, expression, SpelMessage.NOT_ASSIGNABLE); + } + + private void assertIncrementDisabled(SimpleEvaluationContext context, String expression) { + assertEvaluationException(context, expression, SpelMessage.OPERAND_NOT_INCREMENTABLE); + } + + private void assertDecrementDisabled(SimpleEvaluationContext context, String expression) { + assertEvaluationException(context, expression, SpelMessage.OPERAND_NOT_DECREMENTABLE); + } + + private void assertEvaluationException(SimpleEvaluationContext context, String expression, SpelMessage spelMessage) { + assertThatSpelEvaluationException() + .isThrownBy(() -> parser.parseExpression(expression).getValue(context, model)) + .satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(spelMessage)); + } + + + static class Model { + + private String name = "replace me"; + private int count = 0; + private final String[] array = {"replace me"}; + private final int[] numbers = {99}; + private final List list = new ArrayList<>(); + private final Map map = new HashMap<>(); + + Model() { + this.list.add("replace me"); + this.map.put("red", "replace me"); + this.map.put("yellow", "replace me"); + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public int getCount() { + return this.count; + } + + public void setCount(int count) { + this.count = count; + } + + public String[] getArray() { + return this.array; + } + + public int[] getNumbers() { + return this.numbers; + } + + public List getList() { + return this.list; + } + + public Map getMap() { + return this.map; + } + + } + +} From 6c062399eafcd6b8e9efd8c17d77f81d2743c404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 7 Aug 2024 09:50:32 +0200 Subject: [PATCH 254/261] Start building against Reactor 2022.0.22 snapshots See gh-33324 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index d0679158ef98..cbe3fafca3ad 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.10.13")) api(platform("io.netty:netty-bom:4.1.111.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.20")) + api(platform("io.projectreactor:reactor-bom:2022.0.22-SNAPSHOT")) api(platform("io.rsocket:rsocket-bom:1.1.3")) api(platform("org.apache.groovy:groovy-bom:4.0.22")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From b7ca746493ce05e317a50d3c2b4d3d6378797c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 9 Aug 2024 16:05:23 +0200 Subject: [PATCH 255/261] Sync GHA setup --- .../actions/await-http-resource/action.yml | 20 +++++++++++++++++++ .github/actions/build/action.yml | 8 ++++---- .../actions/create-github-release/action.yml | 4 ++-- .../actions/prepare-gradle-build/action.yml | 12 +++++------ .../actions/sync-to-maven-central/action.yml | 15 ++++---------- .../workflows/build-and-deploy-snapshot.yml | 1 + .github/workflows/ci.yml | 3 ++- .github/workflows/release.yml | 2 +- .github/workflows/validate-gradle-wrapper.yml | 2 +- .github/workflows/verify.yml | 2 +- 10 files changed, 42 insertions(+), 27 deletions(-) create mode 100644 .github/actions/await-http-resource/action.yml diff --git a/.github/actions/await-http-resource/action.yml b/.github/actions/await-http-resource/action.yml new file mode 100644 index 000000000000..7d2b3462b537 --- /dev/null +++ b/.github/actions/await-http-resource/action.yml @@ -0,0 +1,20 @@ +name: Await HTTP Resource +description: Waits for an HTTP resource to be available (a HEAD request succeeds) +inputs: + url: + description: 'The URL of the resource to await' + required: true +runs: + using: composite + steps: + - name: Await HTTP resource + shell: bash + run: | + url=${{ inputs.url }} + echo "Waiting for $url" + until curl --fail --head --silent ${{ inputs.url }} > /dev/null + do + echo "." + sleep 60 + done + echo "$url is available" diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 5bd860139947..78188ae631b9 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -5,10 +5,10 @@ inputs: required: false default: '17' description: 'The Java version to compile and test with' - java-distribution: + java-early-access: required: false - default: 'liberica' - description: 'The Java distribution to use for the build' + default: 'false' + description: 'Whether the Java version is in early access' java-toolchain: required: false default: 'false' @@ -35,7 +35,7 @@ runs: with: develocity-access-key: ${{ inputs.develocity-access-key }} java-version: ${{ inputs.java-version }} - java-distribution: ${{ inputs.java-distribution }} + java-early-access: ${{ inputs.java-early-access }} java-toolchain: ${{ inputs.java-toolchain }} - name: Build id: build diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml index 0354737e5dfb..e0120764f1e4 100644 --- a/.github/actions/create-github-release/action.yml +++ b/.github/actions/create-github-release/action.yml @@ -2,10 +2,10 @@ name: Create GitHub Release description: Create the release on GitHub with a changelog inputs: milestone: - description: 'Name of the GitHub milestone for which a release will be created' + description: Name of the GitHub milestone for which a release will be created required: true token: - description: 'Token to use for authentication with GitHub' + description: Token to use for authentication with GitHub required: true runs: using: composite diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml index 0505951dc36e..e4dec8c7f0dd 100644 --- a/.github/actions/prepare-gradle-build/action.yml +++ b/.github/actions/prepare-gradle-build/action.yml @@ -5,10 +5,10 @@ inputs: required: false default: '17' description: 'The Java version to use for the build' - java-distribution: + java-early-access: required: false - default: 'liberica' - description: 'The Java distribution to use for the build' + default: 'false' + description: 'Whether the Java version is in early access' java-toolchain: required: false default: 'false' @@ -22,12 +22,12 @@ runs: - name: Set Up Java uses: actions/setup-java@v4 with: - distribution: ${{ inputs.java-distribution }} + distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || 'liberica' }} java-version: | - ${{ inputs.java-version }} + ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} ${{ inputs.java-toolchain == 'true' && '17' || '' }} - name: Set Up Gradle - uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 with: cache-read-only: false develocity-access-key: ${{ inputs.develocity-access-key }} diff --git a/.github/actions/sync-to-maven-central/action.yml b/.github/actions/sync-to-maven-central/action.yml index 71d17baf73c7..d4e86caf1196 100644 --- a/.github/actions/sync-to-maven-central/action.yml +++ b/.github/actions/sync-to-maven-central/action.yml @@ -20,7 +20,7 @@ runs: using: composite steps: - name: Set Up JFrog CLI - uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.1.2 + uses: jfrog/setup-jfrog-cli@105617d23456a69a92485207c4f28ae12297581d # v4.2.1 env: JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }} - name: Download Release Artifacts @@ -38,13 +38,6 @@ runs: release: true generate-checksums: true - name: Await - shell: bash - run: | - url=${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }} - echo "Waiting for $url" - until curl --fail --head --silent $url > /dev/null - do - echo "." - sleep 60 - done - echo "$url is available" + uses: ./.github/actions/await-http-resource + with: + url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }} diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index eb2e0c0ece67..b1d8d240802d 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -9,6 +9,7 @@ jobs: build-and-deploy-snapshot: name: Build and Deploy Snapshot runs-on: ubuntu-latest + timeout-minutes: 60 if: ${{ github.repository == 'spring-projects/spring-framework' }} steps: - name: Check Out Code diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47a59ea4c6dc..2d3a5e3ec16a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ jobs: ci: name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}' runs-on: ${{ matrix.os.id }} + timeout-minutes: 60 if: ${{ github.repository == 'spring-projects/spring-framework' }} strategy: matrix: @@ -39,7 +40,7 @@ jobs: uses: ./.github/actions/build with: java-version: ${{ matrix.java.version }} - java-distribution: ${{ matrix.java.distribution || 'liberica' }} + java-early-access: ${{ matrix.java.early-access || 'false' }} java-toolchain: ${{ matrix.java.toolchain }} develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - name: Send Notification diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7cdf04194e77..911cf05d2282 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up JFrog CLI - uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.1.2 + uses: jfrog/setup-jfrog-cli@105617d23456a69a92485207c4f28ae12297581d # v4.2.1 env: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - name: Promote build diff --git a/.github/workflows/validate-gradle-wrapper.yml b/.github/workflows/validate-gradle-wrapper.yml index e1629a5f5fe1..7a473b3afe72 100644 --- a/.github/workflows/validate-gradle-wrapper.yml +++ b/.github/workflows/validate-gradle-wrapper.yml @@ -8,4 +8,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: gradle/actions/wrapper-validation@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index b38288a1723b..b9b1e17de783 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -40,7 +40,7 @@ jobs: distribution: 'liberica' java-version: 17 - name: Set Up Gradle - uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + uses: gradle/actions/setup-gradle@d9c87d481d55275bb5441eef3fe0e46805f9ef70 # v3.5.0 with: cache-read-only: false - name: Configure Gradle Properties From f78b09fddf2df0466486e1b17535f648e03bf536 Mon Sep 17 00:00:00 2001 From: Riley Park Date: Mon, 12 Aug 2024 19:45:30 -0700 Subject: [PATCH 256/261] Fix incorrect weak ETag assertion Closes gh-33376 --- .../main/java/org/springframework/http/HttpHeaders.java | 7 +++---- .../java/org/springframework/http/HttpHeadersTests.java | 8 +++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index d1656fbf843d..abe0ef07114a 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -1044,9 +1044,8 @@ public long getDate() { */ public void setETag(@Nullable String etag) { if (etag != null) { - Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/"), - "Invalid ETag: does not start with W/ or \""); - Assert.isTrue(etag.endsWith("\""), "Invalid ETag: does not end with \""); + Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/\""), "ETag does not start with W/\" or \""); + Assert.isTrue(etag.endsWith("\""), "ETag does not end with \""); set(ETAG, etag); } else { diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index ad9f9b9945a6..ef5d90c744c7 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -192,11 +192,17 @@ void ipv6Host() { } @Test - void illegalETag() { + void illegalETagWithoutQuotes() { String eTag = "v2.6"; assertThatIllegalArgumentException().isThrownBy(() -> headers.setETag(eTag)); } + @Test + void illegalWeakETagWithoutLeadingQuote() { + String etag = "W/v2.6\""; + assertThatIllegalArgumentException().isThrownBy(() -> headers.setETag(etag)); + } + @Test void ifMatch() { String ifMatch = "\"v2.6\""; From 79c7bfdbadb73bdabf5412d477454bdeddb67aa5 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:26:34 +0300 Subject: [PATCH 257/261] Introduce withAssignmentDisabled() option for SimpleEvaluationContext To support additional use cases, this commit introduces a withAssignmentDisabled() method in the Builder for SimpleEvaluationContext. See gh-33319 Closes gh-33321 (cherry picked from commit e74406afd09fefb3bac59c8e0feef3a9aceead1d) --- .../spel/support/SimpleEvaluationContext.java | 52 +++++++++++------- .../support/SimpleEvaluationContextTests.java | 53 +++++++++++++++++++ 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index 8e0f11be50f4..f10dfa3718d0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -64,8 +64,9 @@ * read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly, * {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access * to properties. Alternatively, configure custom accessors via - * {@link SimpleEvaluationContext#forPropertyAccessors} and potentially activate method - * resolution and/or a type converter through the builder. + * {@link SimpleEvaluationContext#forPropertyAccessors}, potentially + * {@linkplain Builder#withAssignmentDisabled() disable assignment}, and optionally + * activate method resolution and/or a type converter through the builder. * *

      Note that {@code SimpleEvaluationContext} is typically not configured * with a default root object. Instead it is meant to be created once and @@ -234,9 +235,8 @@ public Object lookupVariable(String name) { * ({@code ++}), and decrement ({@code --}) operators are disabled. * @return {@code true} if assignment is enabled; {@code false} otherwise * @since 5.3.38 - * @see #forPropertyAccessors(PropertyAccessor...) * @see #forReadOnlyDataBinding() - * @see #forReadWriteDataBinding() + * @see Builder#withAssignmentDisabled() */ @Override public boolean isAssignmentEnabled() { @@ -245,15 +245,18 @@ public boolean isAssignmentEnabled() { /** * Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor} - * delegates: typically a custom {@code PropertyAccessor} specific to a use case - * (e.g. attribute resolution in a custom data structure), potentially combined with - * a {@link DataBindingPropertyAccessor} if property dereferences are needed as well. - *

      Assignment is enabled within expressions evaluated by the context created via - * this factory method. + * delegates: typically a custom {@code PropertyAccessor} specific to a use case — + * for example, for attribute resolution in a custom data structure — potentially + * combined with a {@link DataBindingPropertyAccessor} if property dereferences are + * needed as well. + *

      By default, assignment is enabled within expressions evaluated by the context + * created via this factory method; however, assignment can be disabled via + * {@link Builder#withAssignmentDisabled()}. * @param accessors the accessor delegates to use * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see DataBindingPropertyAccessor#forReadWriteAccess() * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { for (PropertyAccessor accessor : accessors) { @@ -262,7 +265,7 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { "ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass."); } } - return new Builder(true, accessors); + return new Builder(accessors); } /** @@ -273,22 +276,26 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see #forPropertyAccessors * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forReadOnlyDataBinding() { - return new Builder(false, DataBindingPropertyAccessor.forReadOnlyAccess()); + return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()).withAssignmentDisabled(); } /** * Create a {@code SimpleEvaluationContext} for read-write access to * public properties via {@link DataBindingPropertyAccessor}. - *

      Assignment is enabled within expressions evaluated by the context created via - * this factory method. + *

      By default, assignment is enabled within expressions evaluated by the context + * created via this factory method. Assignment can be disabled via + * {@link Builder#withAssignmentDisabled()}; however, it is preferable to use + * {@link #forReadOnlyDataBinding()} if you desire read-only access. * @see DataBindingPropertyAccessor#forReadWriteAccess() * @see #forPropertyAccessors * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forReadWriteDataBinding() { - return new Builder(true, DataBindingPropertyAccessor.forReadWriteAccess()); + return new Builder(DataBindingPropertyAccessor.forReadWriteAccess()); } @@ -307,15 +314,24 @@ public static final class Builder { @Nullable private TypedValue rootObject; - private final boolean assignmentEnabled; + private boolean assignmentEnabled = true; - private Builder(boolean assignmentEnabled, PropertyAccessor... accessors) { - this.assignmentEnabled = assignmentEnabled; + private Builder(PropertyAccessor... accessors) { this.accessors = Arrays.asList(accessors); } + /** + * Disable assignment within expressions evaluated by this evaluation context. + * @since 5.3.38 + * @see SimpleEvaluationContext#isAssignmentEnabled() + */ + public Builder withAssignmentDisabled() { + this.assignmentEnabled = false; + return this; + } + /** * Register the specified {@link MethodResolver} delegates for * a combination of property access and method resolution. @@ -347,7 +363,6 @@ public Builder withInstanceMethods() { return this; } - /** * Register a custom {@link ConversionService}. *

      By default a {@link StandardTypeConverter} backed by a @@ -359,6 +374,7 @@ public Builder withConversionService(ConversionService conversionService) { this.typeConverter = new StandardTypeConverter(conversionService); return this; } + /** * Register a custom {@link TypeConverter}. *

      By default a {@link StandardTypeConverter} backed by a diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java index 7ac2132883c3..e2106ac25a35 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java @@ -187,6 +187,59 @@ void forPropertyAccessorsInMixedReadOnlyMode() { assertIncrementAndDecrementWritesForIndexedStructures(context); } + @Test + void forPropertyAccessorsWithAssignmentDisabled() { + SimpleEvaluationContext context = SimpleEvaluationContext + .forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess()) + .withAssignmentDisabled() + .build(); + + assertCommonReadOnlyModeBehavior(context); + + // Map -- with key as property name supported by CompilableMapAccessor + + Expression expression; + expression = parser.parseExpression("map.yellow"); + // setValue() is supported even though assignment is not. + expression.setValue(context, model, "pineapple"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple"); + + // WRITE -- via assignment operator + + // Variable + assertAssignmentDisabled(context, "#myVar = 'rejected'"); + + // Property + assertAssignmentDisabled(context, "name = 'rejected'"); + assertAssignmentDisabled(context, "map.yellow = 'rejected'"); + assertIncrementDisabled(context, "count++"); + assertIncrementDisabled(context, "++count"); + assertDecrementDisabled(context, "count--"); + assertDecrementDisabled(context, "--count"); + + // Array Index + assertAssignmentDisabled(context, "array[0] = 'rejected'"); + assertIncrementDisabled(context, "numbers[0]++"); + assertIncrementDisabled(context, "++numbers[0]"); + assertDecrementDisabled(context, "numbers[0]--"); + assertDecrementDisabled(context, "--numbers[0]"); + + // List Index + assertAssignmentDisabled(context, "list[0] = 'rejected'"); + + // Map Index -- key as String + assertAssignmentDisabled(context, "map['red'] = 'rejected'"); + + // Map Index -- key as pseudo property name + assertAssignmentDisabled(context, "map[yellow] = 'rejected'"); + + // String Index + assertAssignmentDisabled(context, "name[0] = 'rejected'"); + + // Object Index + assertAssignmentDisabled(context, "['name'] = 'rejected'"); + } + private void assertReadWriteMode(SimpleEvaluationContext context) { // Variables can always be set programmatically within an EvaluationContext. From eff1ca92f00ba68ca8f3db8819290d0237b33cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 13 Aug 2024 17:24:20 +0200 Subject: [PATCH 258/261] Upgrade to Reactor 2022.0.22 Closes gh-33324 --- framework-platform/framework-platform.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index cbe3fafca3ad..2b4d6eb07760 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -11,7 +11,7 @@ dependencies { api(platform("io.micrometer:micrometer-bom:1.10.13")) api(platform("io.netty:netty-bom:4.1.111.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2022.0.22-SNAPSHOT")) + api(platform("io.projectreactor:reactor-bom:2022.0.22")) api(platform("io.rsocket:rsocket-bom:1.1.3")) api(platform("org.apache.groovy:groovy-bom:4.0.22")) api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) From b5377ee9cf1ce775307ea80833d6fad0218d3aa1 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 13 Aug 2024 19:34:30 +0200 Subject: [PATCH 259/261] Polishing --- .../support/DefaultListableBeanFactory.java | 12 +++++----- .../hint/BindingReflectionHintsRegistrar.java | 22 +++++++++---------- .../springframework/http/CacheControl.java | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) 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 d8c54764cd29..f0bb35b955b9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -121,16 +121,16 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { @Nullable - private static Class javaxInjectProviderClass; + private static Class jakartaInjectProviderClass; static { try { - javaxInjectProviderClass = + jakartaInjectProviderClass = ClassUtils.forName("jakarta.inject.Provider", DefaultListableBeanFactory.class.getClassLoader()); } catch (ClassNotFoundException ex) { // JSR-330 API not available - Provider interface simply not supported then. - javaxInjectProviderClass = null; + jakartaInjectProviderClass = null; } } @@ -1327,7 +1327,7 @@ else if (ObjectFactory.class == descriptor.getDependencyType() || ObjectProvider.class == descriptor.getDependencyType()) { return new DependencyObjectProvider(descriptor, requestingBeanName); } - else if (javaxInjectProviderClass == descriptor.getDependencyType()) { + else if (jakartaInjectProviderClass == descriptor.getDependencyType()) { return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName); } else { @@ -1757,7 +1757,7 @@ else if (candidatePriority < highestPriority) { * Return whether the bean definition for the given bean name has been * marked as a primary bean. * @param beanName the name of the bean - * @param beanInstance the corresponding bean instance (can be null) + * @param beanInstance the corresponding bean instance (can be {@code null}) * @return whether the given bean qualifies as primary */ protected boolean isPrimary(String beanName, Object beanInstance) { diff --git a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java index 9e5173ab7e66..35a10dfba709 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/BindingReflectionHintsRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -40,7 +40,7 @@ /** * Register the necessary reflection hints so that the specified type can be - * bound at runtime. Fields, constructors, properties and record components + * bound at runtime. Fields, constructors, properties, and record components * are registered, except for a set of types like those in the {@code java.} * package where just the type is registered. Types are discovered transitively * on properties and record components, and generic types are registered as well. @@ -54,8 +54,9 @@ public class BindingReflectionHintsRegistrar { private static final String JACKSON_ANNOTATION = "com.fasterxml.jackson.annotation.JacksonAnnotation"; - private static final boolean jacksonAnnotationPresent = ClassUtils.isPresent(JACKSON_ANNOTATION, - BindingReflectionHintsRegistrar.class.getClassLoader()); + private static final boolean jacksonAnnotationPresent = + ClassUtils.isPresent(JACKSON_ANNOTATION, BindingReflectionHintsRegistrar.class.getClassLoader()); + /** * Register the necessary reflection hints to bind the specified types. @@ -94,8 +95,7 @@ private void registerReflectionHints(ReflectionHints hints, Set seen, Type registerRecordHints(hints, seen, recordComponent.getAccessor()); } } - typeHint.withMembers( - MemberCategory.DECLARED_FIELDS, + typeHint.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); for (Method method : clazz.getMethods()) { String methodName = method.getName(); @@ -132,8 +132,7 @@ private void registerRecordHints(ReflectionHints hints, Set seen, Method m } private void registerPropertyHints(ReflectionHints hints, Set seen, @Nullable Method method, int parameterIndex) { - if (method != null && method.getDeclaringClass() != Object.class && - method.getDeclaringClass() != Enum.class) { + if (method != null && method.getDeclaringClass() != Object.class && method.getDeclaringClass() != Enum.class) { hints.registerMethod(method, ExecutableMode.INVOKE); MethodParameter methodParameter = MethodParameter.forExecutable(method, parameterIndex); Type methodParameterType = methodParameter.getGenericParameterType(); @@ -191,13 +190,13 @@ private void forEachJacksonAnnotation(AnnotatedElement element, Consumer annotation) { - annotation.getRoot().asMap().forEach((key,value) -> { + annotation.getRoot().asMap().forEach((attributeName, value) -> { if (value instanceof Class classValue && value != Void.class) { - if (key.equals("builder")) { + if (attributeName.equals("builder")) { hints.registerType(classValue, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS); } @@ -208,6 +207,7 @@ private void registerHintsForClassAttributes(ReflectionHints hints, MergedAnnota }); } + /** * Inner class to avoid a hard dependency on Kotlin at runtime. */ diff --git a/spring-web/src/main/java/org/springframework/http/CacheControl.java b/spring-web/src/main/java/org/springframework/http/CacheControl.java index 4cb693cf8f91..00b32d8aa483 100644 --- a/spring-web/src/main/java/org/springframework/http/CacheControl.java +++ b/spring-web/src/main/java/org/springframework/http/CacheControl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -393,7 +393,7 @@ private String toHeaderValue() { } private void appendDirective(StringBuilder builder, String value) { - if (builder.length() > 0) { + if (!builder.isEmpty()) { builder.append(", "); } builder.append(value); From ab161ca796642396c87e84aef3a6ab2ff7b5f808 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 13 Aug 2024 19:34:50 +0200 Subject: [PATCH 260/261] Upgrade to Netty 4.1.112 and Undertow 2.3.15 --- framework-platform/framework-platform.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 2b4d6eb07760..69e5ad341f2b 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -9,7 +9,7 @@ javaPlatform { dependencies { api(platform("com.fasterxml.jackson:jackson-bom:2.14.3")) api(platform("io.micrometer:micrometer-bom:1.10.13")) - api(platform("io.netty:netty-bom:4.1.111.Final")) + api(platform("io.netty:netty-bom:4.1.112.Final")) api(platform("io.netty:netty5-bom:5.0.0.Alpha5")) api(platform("io.projectreactor:reactor-bom:2022.0.22")) api(platform("io.rsocket:rsocket-bom:1.1.3")) @@ -54,9 +54,9 @@ dependencies { api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") api("io.reactivex.rxjava3:rxjava:3.1.8") api("io.smallrye.reactive:mutiny:1.10.0") - api("io.undertow:undertow-core:2.3.14.Final") - api("io.undertow:undertow-servlet:2.3.14.Final") - api("io.undertow:undertow-websockets-jsr:2.3.14.Final") + api("io.undertow:undertow-core:2.3.15.Final") + api("io.undertow:undertow-servlet:2.3.15.Final") + api("io.undertow:undertow-websockets-jsr:2.3.15.Final") api("io.vavr:vavr:0.10.4") api("jakarta.activation:jakarta.activation-api:2.0.1") api("jakarta.annotation:jakarta.annotation-api:2.0.0") From 8d16a50907c11f7e6b407d878a26e84eba08a533 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 14 Aug 2024 07:29:50 +0300 Subject: [PATCH 261/261] Efficient ETag parsing Closes gh-33372 --- .../java/org/springframework/http/ETag.java | 144 ++++++++++++++++++ .../org/springframework/http/HttpHeaders.java | 44 ++---- .../context/request/ServletWebRequest.java | 20 +-- .../ServletWebRequestHttpMethodsTests.java | 4 +- 4 files changed, 166 insertions(+), 46 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/ETag.java diff --git a/spring-web/src/main/java/org/springframework/http/ETag.java b/spring-web/src/main/java/org/springframework/http/ETag.java new file mode 100644 index 000000000000..e279ac6a05b4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/ETag.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2024 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; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.StringUtils; + +/** + * Represents an ETag for HTTP conditional requests. + * + * @param tag the unquoted tag value + * @param weak whether the entity tag is for weak or strong validation + * @author Rossen Stoyanchev + * @since 5.3.38 + * @see RFC 7232 + */ +public record ETag(String tag, boolean weak) { + + private static final Log logger = LogFactory.getLog(ETag.class); + + private static final ETag WILDCARD = new ETag("*", false); + + + /** + * Whether this a wildcard tag matching to any entity tag value. + */ + public boolean isWildcard() { + return (this == WILDCARD); + } + + /** + * Return the fully formatted tag including "W/" prefix and quotes. + */ + public String formattedTag() { + if (isWildcard()) { + return "*"; + } + return (this.weak ? "W/" : "") + "\"" + this.tag + "\""; + } + + @Override + public String toString() { + return formattedTag(); + } + + + /** + * Parse entity tags from an "If-Match" or "If-None-Match" header. + * @param source the source string to parse + * @return the parsed ETags + */ + public static List parse(String source) { + + List result = new ArrayList<>(); + State state = State.BEFORE_QUOTES; + int startIndex = -1; + boolean weak = false; + + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); + + if (state == State.IN_QUOTES) { + if (c == '"') { + String tag = source.substring(startIndex, i); + if (StringUtils.hasText(tag)) { + result.add(new ETag(tag, weak)); + } + state = State.AFTER_QUOTES; + startIndex = -1; + weak = false; + } + continue; + } + + if (Character.isWhitespace(c)) { + continue; + } + + if (c == ',') { + state = State.BEFORE_QUOTES; + continue; + } + + if (state == State.BEFORE_QUOTES) { + if (c == '*') { + result.add(WILDCARD); + state = State.AFTER_QUOTES; + continue; + } + if (c == '"') { + state = State.IN_QUOTES; + startIndex = i + 1; + continue; + } + if (c == 'W' && source.length() > i + 2) { + if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') { + state = State.IN_QUOTES; + i = i + 2; + startIndex = i + 1; + weak = true; + continue; + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Unexpected char at index " + i); + } + } + + if (state != State.IN_QUOTES && logger.isDebugEnabled()) { + logger.debug("Expected closing '\"'"); + } + + return result; + } + + + private enum State { + + BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES + + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index abe0ef07114a..94aa9a99aeb3 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -41,8 +41,6 @@ import java.util.Set; import java.util.StringJoiner; import java.util.function.BiConsumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import org.springframework.lang.Nullable; @@ -394,12 +392,6 @@ public class HttpHeaders implements MultiValueMap, Serializable */ public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>()); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see Section 2.3 of RFC 7232 - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH); private static final ZoneId GMT = ZoneId.of("GMT"); @@ -1625,35 +1617,27 @@ public void clearContentHeaders() { /** * Retrieve a combined result from the field values of the ETag header. - * @param headerName the header name + * @param name the header name * @return the combined result * @throws IllegalArgumentException if parsing fails * @since 4.3 */ - protected List getETagValuesAsList(String headerName) { - List values = get(headerName); - if (values != null) { - List result = new ArrayList<>(); - for (String value : values) { - if (value != null) { - Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value); - while (matcher.find()) { - if ("*".equals(matcher.group())) { - result.add(matcher.group()); - } - else { - result.add(matcher.group(1)); - } - } - if (result.isEmpty()) { - throw new IllegalArgumentException( - "Could not parse header '" + headerName + "' with value '" + value + "'"); - } + protected List getETagValuesAsList(String name) { + List values = get(name); + if (values == null) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (String value : values) { + if (value != null) { + List tags = ETag.parse(value); + Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'"); + for (ETag tag : tags) { + result.add(tag.formattedTag()); } } - return result; } - return Collections.emptyList(); + return result; } /** diff --git a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java index 4d5eda127a08..4dd90ef2539b 100644 --- a/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java +++ b/spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -25,13 +25,12 @@ import java.util.Map; import java.util.Set; import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import org.springframework.http.ETag; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -53,12 +52,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final Set SAFE_METHODS = Set.of("GET", "HEAD"); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see Section 2.3 of RFC 7232 - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - /** * Date formats as specified in the HTTP RFC. * @see Section 7.1.1.1 of RFC 7231 @@ -255,20 +248,19 @@ private boolean matchRequestedETags(Enumeration requestedETags, @Nullabl eTag = padEtagIfNecessary(eTag); while (requestedETags.hasMoreElements()) { // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3 - Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(requestedETags.nextElement()); - while (eTagMatcher.find()) { + for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) { // only consider "lost updates" checks for unsafe HTTP methods - if ("*".equals(eTagMatcher.group()) && StringUtils.hasLength(eTag) + if (requestedETag.isWildcard() && StringUtils.hasLength(eTag) && !SAFE_METHODS.contains(getRequest().getMethod())) { return false; } if (weakCompare) { - if (eTagWeakMatch(eTag, eTagMatcher.group(1))) { + if (eTagWeakMatch(eTag, requestedETag.formattedTag())) { return false; } } else { - if (eTagStrongMatch(eTag, eTagMatcher.group(1))) { + if (eTagStrongMatch(eTag, requestedETag.formattedTag())) { return false; } } diff --git a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java index 81cdb8e47f95..7fdc186b6a1f 100644 --- a/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java +++ b/spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -163,8 +163,8 @@ void ifNoneMatchShouldNotMatchDifferentETag(String method) { assertOkWithETag(etag); } + // gh-19127 @SafeHttpMethodsTest - // SPR-14559 void ifNoneMatchShouldNotFailForUnquotedETag(String method) { setUpRequest(method); String etag = "\"etagvalue\"";