Compare commits

...

41 Commits

Author SHA1 Message Date
Spring Builds f32e749dc0 Release v6.0.15 2023-12-14 08:40:53 +00:00
Juergen Hoeller ccaecab500 Polishing 2023-12-14 08:32:08 +01:00
Sam Brannen 67e03105b5 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 8d4deca2a6)
2023-12-13 15:12:29 +01:00
Juergen Hoeller 76bc9cf325 Prepare method overrides when bean class gets resolved
See gh-31826
See gh-31828

(cherry picked from commit cd64e6676c)
2023-12-13 15:11:29 +01:00
rstoyanchev db52c77cca 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.
2023-12-12 20:22:29 +00:00
Sam Brannen 1e742aae34 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 75da9c3c47)
2023-12-12 18:39:38 +01:00
Sam Brannen 20dd585c93 Polish MergedAnnotation tests
(cherry picked from commit 952223dcf9)
2023-12-12 18:38:06 +01:00
Juergen Hoeller 707eb701dc Polishing 2023-12-12 14:02:02 +01:00
Juergen Hoeller 2c97996796 Upgrade to Reactor 2022.0.14
Includes Groovy 4.0.16 and Mockito 5.8

Closes gh-31815
2023-12-12 14:01:55 +01:00
rstoyanchev 3a068b807b Update link to stompjs library
Closes gh-28409
2023-12-12 12:32:21 +00:00
Stéphane Nicoll f54b19ff90 Start building against Reactor 2022.0.14 snapshots
See gh-31815
2023-12-11 15:07:32 +01:00
Stéphane Nicoll 494d2ab727 Remove .mailmap file
See gh-31740
2023-12-11 11:08:31 +01:00
Juergen Hoeller 627d9cf8be Polishing 2023-12-10 00:26:22 +01:00
Arjen Poutsma dd3a67c7ab 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
2023-12-06 14:49:08 +01:00
Sam Brannen 85cc229063 Fix and polish Javadoc for MimeTypeUtils
(cherry picked from commit 1afea0b144)
2023-12-06 14:31:35 +01:00
Johnny Lim fa95f12be0 Fix condition for "Too many elements" in MimeTypeUtils.sortBySpecificity()
See gh-31254
See gh-31769
Closes gh-31773

(cherry picked from commit 7b95bd72f7)
2023-12-06 14:31:16 +01:00
Sam Brannen 035cc72fc8 Polishing 2023-12-06 12:40:49 +01:00
Arjen Poutsma 29a39b617e 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
2023-12-06 12:21:28 +01:00
Sébastien Deleuze a1471a9266 Document @ModelAttribute usage with native images
Closes gh-31767
2023-12-06 12:18:42 +01:00
Sam Brannen 21793b4f93 Suppress warnings in Gradle build
(cherry picked from commit c05b4ce776)
2023-12-01 15:55:36 +01:00
Sam Brannen 2d255c4d5e Upgrade to Gradle 8.5
Closes gh-31734

(cherry picked from commit 3a53446a2b)
2023-12-01 15:54:40 +01:00
Juergen Hoeller 10391586d1 PathEditor considers single-letter URI scheme as NIO path candidate
Closes gh-29881

(cherry picked from commit c56c304536)
2023-11-30 14:16:37 +01:00
Brian Clozel edadc79835 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
2023-11-29 14:55:57 +01:00
Stéphane Nicoll 3783d31c09 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
2023-11-28 17:05:36 +01:00
Sam Brannen 87730f76b1 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 a15f472898)
2023-11-26 12:14:33 +01:00
Juergen Hoeller 6fae3e150e Consider generics in equals method (for ConversionService caching)
Closes gh-31672

(cherry picked from commit 710373d286)
2023-11-24 23:29:02 +01:00
Juergen Hoeller ad44e8ab0a Filter candidate methods by name first (for more efficient sorting)
Closes gh-28377

(cherry picked from commit 0599320bd8)
2023-11-24 23:28:59 +01:00
Juergen Hoeller aadf96ba92 Properly return loaded type even if identified as reloadable
Closes gh-31668

(cherry picked from commit 8921be18de)
2023-11-24 23:28:57 +01:00
Brian Clozel 2baf064d04 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
2023-11-24 17:37:04 +01:00
Brian Clozel 6121e2f526 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.
2023-11-23 15:53:35 +01:00
Brian Clozel 8e33805d29 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
2023-11-22 20:56:37 +01:00
rstoyanchev 1f19bb2311 Update STOMP WebSocket transport reference docs
Closes gh-31616
2023-11-22 15:32:49 +00:00
Juergen Hoeller 2784410cc6 Polishing 2023-11-22 13:01:03 +01:00
Juergen Hoeller f4ac323409 Test for mixed order across bean factory hierarchy
See gh-28374

(cherry picked from commit 48f3c08395)
2023-11-22 12:46:36 +01:00
rstoyanchev 6db00e63c7 WebSocketMessageBrokerStats implements SmartInitializingSingleton
Closes gh-26536
2023-11-21 17:58:32 +00:00
“7fantasy7” ed613e767a Skip buffer in StreamUtils#copy(String)
(cherry picked from commit 54f87f1ff7)
2023-11-20 21:30:01 +01:00
Juergen Hoeller 65781046cf Polishing
(cherry picked from commit fff50657d2)
2023-11-20 21:23:46 +01:00
Juergen Hoeller 0ee36095e7 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 695559879e)
2023-11-20 21:23:42 +01:00
Stéphane Nicoll 0fc38117df 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
2023-11-20 11:54:55 +01:00
Sam Brannen 86b8a70ce6 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 7d2ea7e7e1)
2023-11-20 11:50:51 +01:00
Spring Builds 4bf759d872 Next development version (v6.0.15-SNAPSHOT) 2023-11-16 14:07:29 +00:00
84 changed files with 1135 additions and 660 deletions
-36
View File
@@ -1,36 +0,0 @@
Juergen Hoeller <jhoeller@vmware.com>
Juergen Hoeller <jhoeller@vmware.com> <jhoeller@pivotal.io>
Juergen Hoeller <jhoeller@vmware.com> <jhoeller@gopivotal.com>
Rossen Stoyanchev <rstoyanchev@vmware.com>
Rossen Stoyanchev <rstoyanchev@vmware.com> <rstoyanchev@pivotal.io>
Rossen Stoyanchev <rstoyanchev@vmware.com> <rstoyanchev@gopivotal.com>
Phillip Webb <pwebb@vmware.com>
Phillip Webb <pwebb@vmware.com> <pwebb@pivotal.io>
Phillip Webb <pwebb@vmware.com> <pwebb@gopivotal.com>
Chris Beams <cbeams@vmware.com>
Chris Beams <cbeams@vmware.com> <cbeams@pivotal.io>
Chris Beams <cbeams@vmware.com> <cbeams@gopivotal.com>
Arjen Poutsma <poutsmaa@vmware.com>
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@pivotal.io>
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@gopivotal.com>
Arjen Poutsma <poutsmaa@vmware.com> <poutsma@mac.com>
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@vmware.com>
Oliver Drotbohm <odrotbohm@vmware.com>
Oliver Drotbohm <odrotbohm@vmware.com> <ogierke@vmware.com>
Oliver Drotbohm <odrotbohm@vmware.com> <ogierke@pivotal.io>
Oliver Drotbohm <odrotbohm@vmware.com> <ogierke@gopivotal.com>
Dave Syer <dsyer@vmware.com>
Dave Syer <dsyer@vmware.com> <dsyer@pivotal.io>
Dave Syer <dsyer@vmware.com> <dsyer@gopivotal.com>
Dave Syer <dsyer@vmware.com> <david_syer@hotmail.com>
Andy Clement <aclement@vmware.com>
Andy Clement <aclement@vmware.com> <aclement@pivotal.io>
Andy Clement <aclement@vmware.com> <aclement@gopivotal.com>
Andy Clement <aclement@vmware.com> <andrew.clement@gmail.com>
Sam Brannen <sbrannen@vmware.com>
Sam Brannen <sbrannen@vmware.com> <sbrannen@pivotal.io>
Sam Brannen <sbrannen@vmware.com> <sam@sambrannen.com>
Simon Basle <sbasle@vmware.com>
Simon Baslé <sbasle@vmware.com>
<dmitry.katsubo@gmail.com> <dmitry.katsubo@gmai.com>
Nick Williams <nicholas@nicholaswilliams.net>
-1
View File
@@ -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/**"
-15
View File
@@ -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
+1 -6
View File
@@ -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"
@@ -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/"}.
* <p>{@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<Project> {
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<String> 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> 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());
}
}
@@ -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 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.
@@ -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 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.
@@ -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
</beans>
----
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
</beans>
----
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]]
@@ -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
@@ -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);
}
}
----
+3 -3
View File
@@ -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.13"))
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")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.0.14-SNAPSHOT
version=6.0.15
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
Binary file not shown.
+1 -1
View File
@@ -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
@@ -159,6 +159,9 @@ import org.springframework.util.StringUtils;
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<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>(4);
@@ -193,9 +196,10 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
this.autowiredAnnotationTypes.add(Autowired.class);
this.autowiredAnnotationTypes.add(Value.class);
ClassLoader classLoader = AutowiredAnnotationBeanPostProcessor.class.getClassLoader();
try {
this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
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 class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
try {
this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
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 class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
@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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
"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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
candidateConstructors = new Constructor<?>[] {primaryConstructor};
}
else {
candidateConstructors = new Constructor<?>[0];
candidateConstructors = EMPTY_CONSTRUCTOR_ARRAY;
}
this.candidateConstructorsCache.put(beanClass, candidateConstructors);
}
@@ -1011,7 +1016,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
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 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
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()));
@@ -191,16 +191,14 @@ public final class AutowiredFieldValueResolver extends AutowiredElementResolver
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;
}
@@ -493,15 +493,13 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
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 {
@@ -731,7 +731,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
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)) {
@@ -1497,7 +1497,11 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
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 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
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
@@ -71,7 +71,7 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy {
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 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy {
@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 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy {
@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);
@@ -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 class PathEditor extends PropertyEditorSupport {
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 @@ public class PathEditor extends PropertyEditorSupport {
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());
}
}
}
@@ -2102,7 +2102,7 @@ class DefaultListableBeanFactoryTests {
}
@Test
void beanProviderWithParentBeanFactoryReuseOrder() {
void beanProviderWithParentBeanFactoryDetectsOrder() {
DefaultListableBeanFactory parentBf = new DefaultListableBeanFactory();
parentBf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
parentBf.registerBeanDefinition("regular", new RootBeanDefinition(TestBean.class));
@@ -2110,10 +2110,36 @@ class DefaultListableBeanFactoryTests {
lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
lbf.setParentBeanFactory(parentBf);
lbf.registerBeanDefinition("low", new RootBeanDefinition(LowPriorityTestBean.class));
Stream<Class<?>> 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<TestBean> testBeanProvider = lbf.getBeanProvider(ResolvableType.forClass(TestBean.class));
List<TestBean> 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 @@ class DefaultListableBeanFactoryTests {
}
private static class PriorityTestBeanFactory {
public static LowPriorityTestBean lowPriorityTestBean() {
return new LowPriorityTestBean();
}
public static HighPriorityTestBean highPriorityTestBean() {
return new HighPriorityTestBean();
}
}
private static class NullTestBeanFactoryBean<T> implements FactoryBean<TestBean> {
@Override
@@ -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")
testRuntimeOnly("org.ehcache:ehcache")
testRuntimeOnly("org.glassfish:jakarta.el")
testImplementation("org.hsqldb:hsqldb")
testRuntimeOnly("com.sun.mail:jakarta.mail")
testRuntimeOnly("org.ehcache:ehcache")
testRuntimeOnly("org.ehcache:jcache")
testRuntimeOnly("org.glassfish:jakarta.el")
}
@@ -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 @@ import java.lang.annotation.Target;
* @author Stephane Nicoll
* @author Sam Brannen
* @since 4.1
* @see Cacheable
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@@ -42,8 +43,10 @@ public @interface CacheConfig {
* Names of the default caches to consider for caching operations defined
* in the annotated class.
* <p>If none is set at the operation level, these are used instead of the default.
* <p>May be used to determine the target cache (or caches), matching the
* qualifier value or the bean names of a specific bean definition.
* <p>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 {};
@@ -70,8 +70,12 @@ public @interface Cacheable {
/**
* Names of the caches in which method invocation results are stored.
* <p>Names may be used to determine the target cache (or caches), matching
* the qualifier value or bean name of a specific bean definition.
* <p>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}.
* <p>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
@@ -547,10 +547,10 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
}
/**
* 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<CacheOperationContext> contexts,
@@ -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.Assert;
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.
*
* <p>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 class ClassPathScanningCandidateComponentProvider implements EnvironmentC
* {@link Controller @Controller} stereotype annotations.
* <p>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 class ClassPathScanningCandidateComponentProvider implements EnvironmentC
/**
* 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 class ClassPathScanningCandidateComponentProvider implements EnvironmentC
}
/**
* 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 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
}
}
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);
}
}
}
@@ -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 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
* public MyService myService() {
* return new MyService();
* }
*
* }</pre>
*
* <p>If the configuration class above is processed, {@code MyHints} will be
@@ -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. */
@@ -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 @@ package org.springframework.ejb.config;
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 @@ class LocalStatelessSessionBeanDefinitionParser extends AbstractJndiLocatingBean
return JndiObjectFactoryBean.class;
}
@Override
protected boolean isEligibleAttribute(String attributeName) {
return (super.isEligibleAttribute(attributeName) &&
BeanUtils.getPropertyDescriptor(JndiObjectFactoryBean.class, extractPropertyName(attributeName)) != null);
}
}
@@ -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 @@ package org.springframework.ejb.config;
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 @@ class RemoteStatelessSessionBeanDefinitionParser extends AbstractJndiLocatingBea
return JndiObjectFactoryBean.class;
}
@Override
protected boolean isEligibleAttribute(String attributeName) {
return (super.isEligibleAttribute(attributeName) &&
BeanUtils.getPropertyDescriptor(JndiObjectFactoryBean.class, extractPropertyName(attributeName)) != null);
}
}
@@ -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 @@ import org.springframework.util.StringUtils;
*/
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 class MetadataNamingStrategy implements ObjectNamingStrategy, Initializin
}
Hashtable<String, String> 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;
}
}
@@ -95,7 +95,7 @@
</xsd:complexType>
</xsd:element>
<xsd:element name="local-slsb" type="jndiLocatingType">
<xsd:element name="local-slsb" type="ejbType">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.jndi.JndiObjectFactoryBean"><![CDATA[
Exposes a reference to a local EJB Stateless SessionBean.
@@ -103,15 +103,56 @@
</xsd:annotation>
</xsd:element>
<xsd:element name="remote-slsb" type="jndiLocatingType">
<xsd:element name="remote-slsb">
<xsd:annotation>
<xsd:documentation source="java:org.springframework.jndi.JndiObjectFactoryBean"><![CDATA[
Exposes a reference to a remote EJB Stateless SessionBean.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="ejbType">
<xsd:attribute name="home-interface" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:java.lang.Class"><![CDATA[
The home interface that will be narrowed to before performing the
parameterless SLSB create() call that returns the actual SLSB proxy.
NOTE: Effectively ignored as of 6.0 in favor of plain JNDI lookups.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="refresh-home-on-connect-failure" type="xsd:boolean" default="false">
<xsd:annotation>
<xsd:documentation><![CDATA[
Controls whether to refresh the EJB home on connect failure.
NOTE: Effectively ignored as of 6.0 in favor of plain JNDI lookups.
Can be turned on to allow for hot restart of the EJB server.
If a cached EJB home throws an RMI exception that indicates a
remote connect failure, a fresh home will be fetched and the
invocation will be retried.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="cache-session-bean" type="xsd:boolean" default="false">
<xsd:annotation>
<xsd:documentation><![CDATA[
Controls whether to cache the actual session bean object.
NOTE: Effectively ignored as of 6.0 in favor of plain JNDI lookups.
Off by default for standard EJB compliance. Turn this flag
on to optimize session bean access for servers that are
known to allow for caching the actual session bean object.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="jndiLocatingType">
<!-- base types -->
<xsd:complexType name="jndiLocatingType" abstract="true">
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:sequence>
@@ -183,6 +224,40 @@
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="ejbType">
<xsd:complexContent>
<xsd:extension base="jndiLocatingType">
<xsd:attribute name="lookup-home-on-startup" type="xsd:boolean" default="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
Controls whether the lookup of the EJB home object is performed
immediately on startup (if true, the default), or on first access
(if false).
NOTE: Effectively ignored as of 6.0 in favor of plain JNDI lookups.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="cache-home" type="xsd:boolean" default="true">
<xsd:annotation>
<xsd:documentation><![CDATA[
Controls whether the EJB home object is cached once it has been located.
On by default; turn this flag off to always reobtain fresh home objects.
NOTE: Effectively ignored as of 6.0 in favor of plain JNDI lookups.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="business-interface" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:java.lang.Class"><![CDATA[
The business interface of the EJB being proxied.
NOTE: Effectively ignored as of 6.0 in favor of plain JNDI lookups.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:simpleType name="environmentRefType">
<xsd:annotation>
<xsd:appinfo>
@@ -1086,8 +1086,8 @@ public abstract class AbstractAopProxyTests {
// 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 abstract class AbstractAopProxyTests {
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 abstract class AbstractAopProxyTests {
// 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 abstract class AbstractAopProxyTests {
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 abstract class AbstractAopProxyTests {
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() {
@@ -22,8 +22,13 @@ import java.io.InputStream;
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.ITestBean;
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 @@ class XmlBeanFactoryTests {
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 @@ class XmlBeanFactoryTests {
}
}
interface EchoService {
String[] echo(Object... objects);
}
class ReverseArrayMethodReplacer implements MethodReplacer {
@Override
public Object reimplement(Object obj, Method method, Object[] args) {
List<String> list = Arrays.stream((Object[]) args[0])
.map(Object::toString)
.collect(Collectors.toCollection(ArrayList::new));
Collections.reverse(list);
return list.toArray(String[]::new);
}
}
@@ -54,7 +54,7 @@ class EnableCachingIntegrationTests {
@AfterEach
public void closeContext() {
void closeContext() {
if (this.context != null) {
this.context.close();
}
@@ -60,6 +60,7 @@ class CacheErrorHandlerTests {
private SimpleService simpleService;
@BeforeEach
void setup() {
this.context = new AnnotationConfigApplicationContext(Config.class);
@@ -69,11 +70,13 @@ class CacheErrorHandlerTests {
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 @@ class CacheErrorHandlerTests {
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 @@ class CacheErrorHandlerTests {
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 @@ class CacheErrorHandlerTests {
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 @@ class CacheErrorHandlerTests {
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");
}
@@ -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 class CachePutEvaluationTests {
}
@AfterEach
public void close() {
if (this.context != null) {
this.context.close();
}
public void closeContext() {
this.context.close();
}
@Test
public void mutualGetPutExclusion() {
String key = "1";
@@ -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 @@ class CacheResolverCustomizationTests {
}
@AfterEach
void tearDown() {
void closeContext() {
this.context.close();
}
@@ -142,16 +142,17 @@ class CacheResolverCustomizationTests {
@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"));
}
@@ -126,16 +126,16 @@ class GenericApplicationContextTests {
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 @@ class GenericApplicationContextTests {
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 @@ class GenericApplicationContextTests {
// java.nio.file.InvalidPathException: Illegal char <:> 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 @@ class GenericApplicationContextTests {
assertThat(resource).isInstanceOf(FileUrlResource.class);
resource = context.getResource(pingLocation);
assertThat(resource).asInstanceOf(type(ByteArrayResource.class))
.extracting(bar -> 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 @@ class GenericApplicationContextTests {
}
}
static class BeanB implements ApplicationContextAware {
static class BeanB implements ApplicationContextAware {
ApplicationContext applicationContext;
@@ -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.core.io.ClassPathResource;
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 class JeeNamespaceHandlerTests {
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 class JeeNamespaceHandlerTests {
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
@@ -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<ObjectName> hasDefaultProperties(Object instance, String expectedName) {
return objectName -> assertThat(objectName.getKeyPropertyList()).containsOnly(
entry("type", ClassUtils.getShortName(instance.getClass())),
entry("name", expectedName));
}
static class TestBean {}
}
@@ -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">
<!--
Not yet in use: illustration of possible approach
-->
<bean id="overrideOneMethod" class="org.springframework.beans.factory.xml.OverrideOneMethod">
<lookup-method name="getPrototypeDependency" bean="jenny"/>
@@ -27,39 +24,34 @@
<lookup-method name="protectedOverrideSingleton" bean="david"/>
<!--
This method is not overloaded, so we don't need to specify any arg types
-->
<!-- This method is not overloaded, so we don't need to specify any arg types -->
<replaced-method name="doSomething" replacer="doSomethingReplacer"/>
</bean>
<bean id="replaceVoidMethod" parent="someParent"
class="org.springframework.beans.factory.xml.OverrideOneMethodSubclass">
<bean id="replaceVoidMethod" parent="someParent" class="org.springframework.beans.factory.xml.OverrideOneMethodSubclass"/>
<bean id="replaceEchoMethod" class="org.springframework.beans.factory.xml.EchoService">
<!-- This method is not overloaded, so we don't need to specify any arg types -->
<replaced-method name="echo" replacer="reverseArrayReplacer" />
</bean>
<bean id="reverseReplacer"
class="org.springframework.beans.factory.xml.ReverseMethodReplacer"/>
<bean id="reverseReplacer" class="org.springframework.beans.factory.xml.ReverseMethodReplacer"/>
<bean id="fixedReplacer"
class="org.springframework.beans.factory.xml.FixedMethodReplacer"/>
<bean id="reverseArrayReplacer" class="org.springframework.beans.factory.xml.ReverseArrayMethodReplacer"/>
<bean id="doSomethingReplacer"
class="org.springframework.beans.factory.xml.XmlBeanFactoryTests$DoSomethingReplacer"/>
<bean id="fixedReplacer" class="org.springframework.beans.factory.xml.FixedMethodReplacer"/>
<bean id="serializableReplacer"
class="org.springframework.beans.factory.xml.SerializableMethodReplacerCandidate">
<bean id="doSomethingReplacer" class="org.springframework.beans.factory.xml.XmlBeanFactoryTests$DoSomethingReplacer"/>
<bean id="serializableReplacer" class="org.springframework.beans.factory.xml.SerializableMethodReplacerCandidate">
<!-- Arbitrary method replacer -->
<replaced-method name="replaceMe" replacer="reverseReplacer">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="jenny" class="org.springframework.beans.testfixture.beans.TestBean"
scope="prototype">
<bean id="jenny" class="org.springframework.beans.testfixture.beans.TestBean" scope="prototype">
<property name="name"><value>Jenny</value></property>
<property name="age"><value>30</value></property>
<property name="spouse">
@@ -68,8 +60,7 @@
</property>
</bean>
<bean id="david" class="org.springframework.beans.testfixture.beans.TestBean"
scope="singleton">
<bean id="david" class="org.springframework.beans.testfixture.beans.TestBean" scope="singleton">
<description>
Simple bean, without any collections.
</description>
@@ -40,14 +40,41 @@
</util:properties>
<!-- Local EJB Tests -->
<jee:local-slsb id="simpleLocalEjb" jndi-name="ejb/MyLocalBean"/>
<jee:local-slsb id="simpleLocalEjb" jndi-name="ejb/MyLocalBean"
business-interface="org.springframework.beans.testfixture.beans.ITestBean"/>
<jee:local-slsb id="complexLocalEjb"
jndi-name="ejb/MyLocalBean"
business-interface="org.springframework.beans.testfixture.beans.ITestBean"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true">
<jee:environment>foo=bar</jee:environment>
</jee:local-slsb>
<!-- Remote EJB Tests -->
<jee:remote-slsb id="simpleRemoteEjb" jndi-name="ejb/MyRemoteBean"/>
<jee:remote-slsb id="simpleRemoteEjb" jndi-name="ejb/MyRemoteBean"
business-interface="org.springframework.beans.testfixture.beans.ITestBean"/>
<!-- Lazy beans Tests-->
<jee:remote-slsb id="complexRemoteEjb"
jndi-name="ejb/MyRemoteBean"
business-interface="org.springframework.beans.testfixture.beans.ITestBean"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true"
home-interface="org.springframework.beans.testfixture.beans.ITestBean"
refresh-home-on-connect-failure="true"
cache-session-bean="true">
<jee:environment>foo=bar</jee:environment>
</jee:remote-slsb>
<!-- Lazy Lookup Tests-->
<jee:jndi-lookup id="lazyDataSource" jndi-name="jdbc/MyDataSource" lazy-init="true"/>
<jee:local-slsb id="lazyLocalBean" jndi-name="ejb/MyLocalBean" lazy-init="true"/>
<jee:remote-slsb id="lazyRemoteBean" jndi-name="ejb/MyRemoteBean" lazy-init="true"/>
<jee:local-slsb id="lazyLocalBean" jndi-name="ejb/MyLocalBean"
business-interface="org.springframework.beans.testfixture.beans.ITestBean" lazy-init="true"/>
<jee:remote-slsb id="lazyRemoteBean" jndi-name="ejb/MyRemoteBean"
business-interface="org.springframework.beans.testfixture.beans.ITestBean" lazy-init="true"/>
</beans>
@@ -20,6 +20,7 @@ import org.springframework.core.io.InputStreamSource;
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 @@ public interface GeneratedFiles {
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++) {
@@ -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 @@ abstract class AnnotationsScanner {
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;
@@ -475,7 +475,7 @@ public class TypeDescriptor implements Serializable {
ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor()));
}
else {
return true;
return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics());
}
}
@@ -330,10 +330,10 @@ public abstract class MimeTypeUtils {
}
/**
* 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<? extends MimeType> mimeTypes) {
StringBuilder builder = new StringBuilder();
@@ -348,21 +348,19 @@ public abstract class MimeTypeUtils {
}
/**
* Sorts the given list of {@code MimeType} objects by
* Sort the given list of {@code MimeType} objects by
* {@linkplain MimeType#isMoreSpecific(MimeType) specificity}.
*
* <p>Because of the computational cost, this method throws an exception
* when the given list contains too many elements.
* <p>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 <a href="https://tools.ietf.org/html/rfc7231#section-5.3.2">HTTP 1.1: Semantics
* and Content, section 5.3.2</a>
* @see MimeType#isMoreSpecific(MimeType)
*/
public static <T extends MimeType> void sortBySpecificity(List<T> 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");
}
@@ -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.
@@ -23,8 +23,6 @@ import java.io.IOException;
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 abstract class StreamUtils {
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();
}
/**
@@ -60,6 +60,15 @@ class GeneratedFilesTests {
.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 @@ class GeneratedFilesTests {
.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()
@@ -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.
@@ -197,7 +197,7 @@ class AnnotationsScannerTests {
}
@Test
void typeHierarchyStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() {
void typeHierarchyStrategyOnClassWhenHasSingleInterfaceScansInterfaces() {
Class<?> source = WithSingleInterface.class;
assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY)).containsExactly(
"0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2");
@@ -353,10 +353,19 @@ class AnnotationsScannerTests {
}
@Test
void typeHierarchyStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() {
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 @@ class AnnotationsScannerTests {
}
}
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 {
@@ -40,6 +40,8 @@ import org.junit.jupiter.api.Nested;
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;
@@ -679,16 +681,23 @@ class MergedAnnotationsTests {
}
@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);
}
@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");
@@ -1384,7 +1393,7 @@ class MergedAnnotationsTests {
}
@Test
void getRepeatableDeclaredOnMethod() throws Exception {
void streamRepeatableDeclaredOnMethod() throws Exception {
Method method = InterfaceWithRepeated.class.getMethod("foo");
Stream<MergedAnnotation<MyRepeatable>> annotations = MergedAnnotations.from(
method, SearchStrategy.TYPE_HIERARCHY).stream(MyRepeatable.class);
@@ -1395,7 +1404,7 @@ class MergedAnnotationsTests {
@Test
@SuppressWarnings("deprecation")
void getRepeatableDeclaredOnClassWithAttributeAliases() {
void streamRepeatableDeclaredOnClassWithAttributeAliases() {
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(
TestConfiguration.class)).isEmpty();
RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class,
@@ -1409,7 +1418,7 @@ class MergedAnnotationsTests {
}
@Test
void getRepeatableDeclaredOnClass() {
void streamRepeatableDeclaredOnClass() {
Class<?> element = MyRepeatableClass.class;
String[] expectedValuesJava = { "A", "B", "C" };
String[] expectedValuesSpring = { "A", "B", "C", "meta1" };
@@ -1417,7 +1426,7 @@ class MergedAnnotationsTests {
}
@Test
void getRepeatableDeclaredOnSuperclass() {
void streamRepeatableDeclaredOnSuperclass() {
Class<?> element = SubMyRepeatableClass.class;
String[] expectedValuesJava = { "A", "B", "C" };
String[] expectedValuesSpring = { "A", "B", "C", "meta1" };
@@ -1425,7 +1434,7 @@ class MergedAnnotationsTests {
}
@Test
void getRepeatableDeclaredOnClassAndSuperclass() {
void streamRepeatableDeclaredOnClassAndSuperclass() {
Class<?> element = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class;
String[] expectedValuesJava = { "X", "Y", "Z" };
String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" };
@@ -1433,7 +1442,7 @@ class MergedAnnotationsTests {
}
@Test
void getRepeatableDeclaredOnMultipleSuperclasses() {
void streamRepeatableDeclaredOnMultipleSuperclasses() {
Class<?> element = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class;
String[] expectedValuesJava = { "X", "Y", "Z" };
String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" };
@@ -1441,7 +1450,7 @@ class MergedAnnotationsTests {
}
@Test
void getDirectRepeatablesDeclaredOnClass() {
void streamDirectRepeatablesDeclaredOnClass() {
Class<?> element = MyRepeatableClass.class;
String[] expectedValuesJava = { "A", "B", "C" };
String[] expectedValuesSpring = { "A", "B", "C", "meta1" };
@@ -1449,7 +1458,7 @@ class MergedAnnotationsTests {
}
@Test
void getDirectRepeatablesDeclaredOnSuperclass() {
void streamDirectRepeatablesDeclaredOnSuperclass() {
Class<?> element = SubMyRepeatableClass.class;
String[] expectedValuesJava = {};
String[] expectedValuesSpring = {};
@@ -1476,20 +1485,17 @@ class MergedAnnotationsTests {
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<String> 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<String> values = MergedAnnotations.from(element, searchStrategy).stream(MyRepeatable.class)
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
.map(annotation -> annotation.getString("value"));
assertThat(values).containsExactly(expected);
}
@Test
@@ -34,11 +34,13 @@ import java.util.HashSet;
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 @@ class TypeDescriptorTests {
}
@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 @@ class TypeDescriptorTests {
}
@Test
void elementTypeForCollectionSubclass() throws Exception {
void elementTypeForCollectionSubclass() {
@SuppressWarnings("serial")
class CustomSet extends HashSet<String> {
}
@@ -692,7 +694,7 @@ class TypeDescriptorTests {
}
@Test
void elementTypeForMapSubclass() throws Exception {
void elementTypeForMapSubclass() {
@SuppressWarnings("serial")
class CustomMap extends HashMap<String, Integer> {
}
@@ -704,7 +706,7 @@ class TypeDescriptorTests {
}
@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 @@ class TypeDescriptorTests {
}
@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 @@ class TypeDescriptorTests {
}
@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 @@ class TypeDescriptorTests {
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
@@ -117,6 +117,7 @@ public class ReflectiveMethodResolver implements MethodResolver {
TypeConverter typeConverter = context.getTypeConverter();
Class<?> type = (targetObject instanceof Class<?> clazz ? clazz : targetObject.getClass());
ArrayList<Method> 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 @@ public class ReflectiveMethodResolver implements MethodResolver {
boolean multipleOptions = false;
for (Method method : methodsToIterate) {
if (method.getName().equals(name)) {
int paramCount = method.getParameterCount();
List<TypeDescriptor> paramDescriptors = new ArrayList<>(paramCount);
for (int i = 0; i < paramCount; i++) {
paramDescriptors.add(new TypeDescriptor(new MethodParameter(method, i)));
int paramCount = method.getParameterCount();
List<TypeDescriptor> 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);
}
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);
}
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) {
@@ -120,9 +120,10 @@ public class StandardTypeLocator implements TypeLocator {
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);
@@ -60,7 +60,7 @@ import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;
/**
* <b>This is the central class in the JDBC core package.</b>
* <b>This is the central delegate in the JDBC core package.</b>
* 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
@@ -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
* <p>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<CallParameterMetaData> getCallParameterMetaData();
/**
* Provide any modification of the procedure name passed in to match the meta-data currently used.
* This could include altering the case.
* <p>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.
* <p>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.
* <p>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
* <p>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 @@ public interface CallMetaDataProvider {
/**
* 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
* <p>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 @@ public interface CallMetaDataProvider {
/**
* Provide any modification of the column name passed in to match the meta-data currently used.
* This could include altering the case.
* <p>This could include altering the case.
* @param parameterName name of the parameter of column
*/
@Nullable
@@ -102,7 +108,7 @@ public interface CallMetaDataProvider {
/**
* Create a default out parameter based on the provided meta-data.
* This is used when no explicit parameter declaration has been made.
* <p>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 @@ public interface CallMetaDataProvider {
/**
* Create a default in/out parameter based on the provided meta-data.
* This is used when no explicit parameter declaration has been made.
* <p>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 @@ public interface CallMetaDataProvider {
/**
* Create a default in parameter based on the provided meta-data.
* This is used when no explicit parameter declaration has been made.
* <p>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 @@ public interface CallMetaDataProvider {
/**
* 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 @@ public interface CallMetaDataProvider {
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?
* <p>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<CallParameterMetaData> getCallParameterMetaData();
/**
* Does the database support the use of catalog name in procedure calls?
*/
@@ -168,11 +168,6 @@ public class GenericCallMetaDataProvider implements CallMetaDataProvider {
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 class GenericCallMetaDataProvider implements CallMetaDataProvider {
return this.procedureColumnMetaDataUsed;
}
@Override
public boolean byPassReturnParameter(String parameterName) {
return false;
}
/**
* Specify whether the database supports the use of catalog name in procedure calls.
@@ -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 abstract class AbstractDataSource implements DataSource {
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 abstract class AbstractDataSource implements DataSource {
return iface.isInstance(this);
}
//---------------------------------------------------------------------
// Implementation of JDBC 4.1's getParentLogger method
//---------------------------------------------------------------------
@Override
public Logger getParentLogger() {
return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
}
}
@@ -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.
@@ -104,6 +104,16 @@ public class DelegatingDataSource implements DataSource, InitializingBean {
return obtainTargetDataSource().getConnection(username, password);
}
@Override
public int getLoginTimeout() throws SQLException {
return obtainTargetDataSource().getLoginTimeout();
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
obtainTargetDataSource().setLoginTimeout(seconds);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return obtainTargetDataSource().getLogWriter();
@@ -115,20 +125,10 @@ public class DelegatingDataSource implements DataSource, InitializingBean {
}
@Override
public int getLoginTimeout() throws SQLException {
return obtainTargetDataSource().getLoginTimeout();
public Logger getParentLogger() {
return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
obtainTargetDataSource().setLoginTimeout(seconds);
}
//---------------------------------------------------------------------
// Implementation of JDBC 4.0's Wrapper interface
//---------------------------------------------------------------------
@Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> iface) throws SQLException {
@@ -143,14 +143,4 @@ public class DelegatingDataSource implements DataSource, InitializingBean {
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);
}
}
@@ -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 class TransactionAwareDataSourceProxy extends DelegatingDataSource {
// 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(']');
@@ -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}
* <p>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}).
* <p>The key can be of arbitrary type; this class implements the
@@ -213,6 +213,7 @@ public abstract class AbstractRoutingDataSource extends AbstractDataSource imple
return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
}
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
@@ -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()
@@ -353,6 +353,11 @@ public class PersistenceAnnotationBeanPostProcessor implements InstantiationAwar
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 @@ public class PersistenceAnnotationBeanPostProcessor implements InstantiationAwar
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);
+1
View File
@@ -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")
@@ -205,8 +205,8 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
.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);
@@ -91,10 +91,12 @@ final class Jackson2Tokenizer {
private List<TokenBuffer> tokenize(DataBuffer dataBuffer) {
try {
int bufferSize = dataBuffer.readableByteCount();
List<TokenBuffer> 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 @@ final class Jackson2Tokenizer {
byte[] bytes = new byte[bufferSize];
dataBuffer.read(bytes);
byteArrayFeeder.feedInput(bytes, 0, bufferSize);
parseTokens(tokens);
}
List<TokenBuffer> 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 @@ final class Jackson2Tokenizer {
return Flux.defer(() -> {
this.inputFeeder.endOfInput();
try {
return Flux.fromIterable(parseTokenBufferFlux());
List<TokenBuffer> 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 @@ final class Jackson2Tokenizer {
});
}
private List<TokenBuffer> parseTokenBufferFlux() throws IOException {
List<TokenBuffer> result = new ArrayList<>();
private void parseTokens(List<TokenBuffer> tokens) throws IOException {
// SPR-16151: Smile data format uses null to separate documents
boolean previousNull = false;
while (!this.parser.isClosed()) {
@@ -153,13 +155,12 @@ final class Jackson2Tokenizer {
}
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) {
@@ -540,7 +540,7 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
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);
@@ -121,16 +121,17 @@ public class ServerHttpObservationFilter implements WebFilter {
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 class ServerHttpObservationFilter implements WebFilter {
@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 class ServerHttpObservationFilter implements WebFilter {
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();
});
}
}
}
@@ -27,6 +27,10 @@ import com.fasterxml.jackson.core.JsonToken;
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 reactor.test.StepVerifier;
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 class Jackson2TokenizerTests extends AbstractLeakCheckingTests {
.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<DataBuffer> source = Flux.just(bufferFactory.wrap(composite));
Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, false, -1);
Flux<String> strings = tokens.map(this::tokenToString);
StepVerifier.create(strings)
.assertNext(s -> assertThat(s).isEqualTo("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"))
.verifyComplete();
}
private Flux<String> decode(List<String> source, boolean tokenize, int maxInMemorySize) {
Flux<TokenBuffer> 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) {
@@ -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<org.springframework.http.codec.json.JacksonCsvEncoderTests.JacksonCsvEncoder> {
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<Object> 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<Object> 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<String, Object> hints) {
var mapper = (CsvMapper) getObjectMapper();
return writer.with(mapper.schemaFor(elementType.toClass()).withHeader());
}
}
}
@@ -301,6 +301,23 @@ class DefaultPartHttpMessageReaderTests {
latch.await();
}
@ParameterizedDefaultPartHttpMessageReaderTest
void emptyLastPart(DefaultPartHttpMessageReader reader) throws InterruptedException {
MockServerHttpRequest request = createRequest(
new ClassPathResource("empty-part.multipart", getClass()), "LiG0chJ0k7YtLt-FzTklYFgz50i88xJCW5jD");
Flux<Part> 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 {
@@ -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 @@ package org.springframework.web.filter.reactive;
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 org.junit.jupiter.api.Test;
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 @@ class ServerHttpObservationFilterTests {
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 @@ class ServerHttpObservationFilterTests {
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<ServerRequestObservationContext> observationContext = ServerHttpObservationFilter.findObservationContext(exchange);
assertThat(observationContext.get().getError()).isInstanceOf(IllegalArgumentException.class);
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SERVER_ERROR");
}
private WebFilterChain createFilterChain(ThrowingConsumer<ServerWebExchange> exchangeConsumer) {
return filterExchange -> {
try {
@@ -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--
@@ -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 @@ import org.springframework.lang.Nullable;
*/
public class ClientRequestObservationContext extends RequestReplySenderContext<ClientRequest.Builder, ClientResponse> {
/**
* 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 class ClientRequestObservationContext extends RequestReplySenderContext<C
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<ClientRequestObservationContext> findCurrent(ClientRequest request) {
return Optional.ofNullable((ClientRequestObservationContext) request.attributes().get(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE));
}
}
@@ -450,7 +450,9 @@ final class DefaultWebClient implements WebClient {
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<ClientResponse> responseMono = filterFunction.apply(exchangeFunction)
@@ -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 @@ public class PathResourceResolver extends AbstractResourceResolver {
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));
}
}
@@ -259,13 +259,19 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte
assertThat(data).hasSize(2);
List<PartEvent> 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<PartEvent> fieldData = data.get(1);
assertThat(fieldData).hasSize(1);
assertThat(fieldData.get(0)).isInstanceOf(FormPartEvent.class);
@@ -152,6 +152,26 @@ class WebClientObservationTests {
verifyAndGetRequest();
}
@Test
void setsCurrentObservationContextAsRequestAttribute() {
ExchangeFilterFunction assertionFilter = new ExchangeFilterFunction() {
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction chain) {
Optional<ClientRequestObservationContext> 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()));
@@ -191,8 +191,12 @@ public class HandlerMappingIntrospector
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 class HandlerMappingIntrospector
* @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 @@ public class HandlerMappingIntrospector
* 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 @@ public class HandlerMappingIntrospector
* @since 6.0.14
*/
@SuppressWarnings("serial")
private static final class CachedResult {
public static final class CachedResult {
private final DispatcherType dispatcherType;
@@ -195,8 +195,8 @@ public class PathResourceResolver extends AbstractResourceResolver {
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));
}
}
@@ -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.
]]></xsd:documentation>
@@ -399,6 +399,7 @@ public class HandlerMappingIntrospectorTests {
}
@SuppressWarnings("serial")
private static class TestServlet extends HttpServlet {
@Override
@@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit;
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 @@ import org.springframework.web.socket.messaging.SubProtocolWebSocketHandler;
* @author Sam Brannen
* @since 4.1
*/
public class WebSocketMessageBrokerStats {
public class WebSocketMessageBrokerStats implements SmartInitializingSingleton {
private static final Log logger = LogFactory.getLog(WebSocketMessageBrokerStats.class);
@@ -84,7 +85,49 @@ public class WebSocketMessageBrokerStats {
public void setSubProtocolWebSocketHandler(SubProtocolWebSocketHandler webSocketHandler) {
this.webSocketHandler = webSocketHandler;
}
public void setStompBrokerRelay(StompBrokerRelayMessageHandler stompBrokerRelay) {
this.stompBrokerRelay = stompBrokerRelay;
}
public void setInboundChannelExecutor(TaskExecutor inboundChannelExecutor) {
this.inboundChannelExecutor = inboundChannelExecutor;
}
public void setOutboundChannelExecutor(TaskExecutor outboundChannelExecutor) {
this.outboundChannelExecutor = outboundChannelExecutor;
}
public void setSockJsTaskScheduler(TaskScheduler sockJsTaskScheduler) {
this.sockJsTaskScheduler = sockJsTaskScheduler;
}
/**
* Set the frequency for logging information at INFO level in milliseconds.
* If set 0 or less than 0, the logging task is cancelled.
* <p>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);
}
}
/**
* Return the configured logging period frequency in milliseconds.
*/
public long getLoggingPeriod() {
return this.loggingPeriod;
}
@Override
public void afterSingletonsInstantiated() {
this.stompSubProtocolHandler = initStompSubProtocolHandler();
this.loggingTask = initLoggingTask(TimeUnit.MINUTES.toMillis(1));
}
@Nullable
@@ -104,23 +147,6 @@ public class WebSocketMessageBrokerStats {
return null;
}
public void setStompBrokerRelay(StompBrokerRelayMessageHandler stompBrokerRelay) {
this.stompBrokerRelay = stompBrokerRelay;
}
public void setInboundChannelExecutor(TaskExecutor inboundChannelExecutor) {
this.inboundChannelExecutor = inboundChannelExecutor;
}
public void setOutboundChannelExecutor(TaskExecutor outboundChannelExecutor) {
this.outboundChannelExecutor = 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()) {
@@ -131,25 +157,6 @@ public class WebSocketMessageBrokerStats {
return null;
}
/**
* Set the frequency for logging information at INFO level in milliseconds.
* If set 0 or less than 0, the logging task is cancelled.
* <p>By default this property is set to 30 minutes (30 * 60 * 1000).
*/
public void setLoggingPeriod(long period) {
if (this.loggingTask != null) {
this.loggingTask.cancel(true);
}
this.loggingPeriod = period;
this.loggingTask = initLoggingTask(0);
}
/**
* Return the configured logging period frequency in milliseconds.
*/
public long getLoggingPeriod() {
return this.loggingPeriod;
}
/**
* Get stats about WebSocket sessions.
@@ -420,7 +420,7 @@
<xsd:annotation>
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
Set the core pool size of the ThreadPoolExecutor.
NOTE: the core pool size is effectively the max pool size when an unbounded queue-capacity is configured (the default).
NOTE: The core pool size is effectively the max pool size when an unbounded queue-capacity is configured (the default).
This is essentially the "Unbounded queues" strategy as explained in java.util.concurrent.ThreadPoolExecutor.
When this strategy is used, the max pool size is effectively ignored.
By default this is set to twice the value of Runtime.availableProcessors().
@@ -433,7 +433,7 @@
<xsd:annotation>
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
Set the max pool size of the ThreadPoolExecutor.
NOTE: when an unbounded queue-capacity is configured (the default), the max pool size is effectively ignored.
NOTE: When an unbounded queue-capacity is configured (the default), the max pool size is effectively ignored.
See the "Unbounded queues" strategy in java.util.concurrent.ThreadPoolExecutor for more details.
By default this is set to Integer.MAX_VALUE.
]]></xsd:documentation>
@@ -453,7 +453,7 @@
<xsd:annotation>
<xsd:documentation source="java:org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"><![CDATA[
Set the queue capacity for the ThreadPoolExecutor.
NOTE: when an unbounded queue-capacity is configured (the default) the core pool size is effectively the max pool size.
NOTE: When an unbounded queue-capacity is configured (the default) the core pool size is effectively the max pool size.
This is essentially the "Unbounded queues" strategy as explained in java.util.concurrent.ThreadPoolExecutor.
When this strategy is used, the max pool size is effectively ignored.
By default this is set to Integer.MAX_VALUE.