Merge branch 'main' into 35179

This commit is contained in:
Nabil Fawwaz Elqayyim 2025-08-24 04:41:14 +07:00
commit 832689925c
No known key found for this signature in database
GPG Key ID: A270A6876336275F
417 changed files with 14330 additions and 7392 deletions

View File

@ -30,6 +30,7 @@ runs:
java-version: | java-version: |
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
${{ inputs.java-toolchain == 'true' && '17' || '' }} ${{ inputs.java-toolchain == 'true' && '17' || '' }}
24
- name: Set Up Gradle - name: Set Up Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with: with:

View File

@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config # Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below # Add key=value pairs of SDKs to use below
java=24.0.1-librca java=24.0.2-librca

View File

@ -2,11 +2,11 @@ plugins {
id 'io.freefair.aspectj' version '8.13.1' apply false id 'io.freefair.aspectj' version '8.13.1' apply false
// kotlinVersion is managed in gradle.properties // kotlinVersion is managed in gradle.properties
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
id 'org.jetbrains.dokka' version '1.9.20' id 'org.jetbrains.dokka'
id 'com.github.bjornvester.xjc' version '1.8.2' apply false id 'com.github.bjornvester.xjc' version '1.8.2' apply false
id 'io.github.goooler.shadow' version '8.1.8' apply false id 'io.github.goooler.shadow' version '8.1.8' apply false
id 'me.champeau.jmh' version '0.7.2' apply false id 'me.champeau.jmh' version '0.7.2' apply false
id "io.spring.nullability" version "0.0.1" apply false id 'io.spring.nullability' version '0.0.4' apply false
} }
ext { ext {
@ -75,7 +75,7 @@ configure([rootProject] + javaProjects) { project ->
"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/", "https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/test/release/api/", "https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/", "https://junit.org/junit4/javadoc/4.13.2/",
"https://docs.junit.org/5.13.3/api/", "https://docs.junit.org/5.13.4/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/", "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/", //"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/", "https://r2dbc.io/spec/1.0.0.RELEASE/api/",

View File

@ -20,6 +20,7 @@ ext {
dependencies { dependencies {
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}" checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.0.0"
implementation "com.tngtech.archunit:archunit:1.4.0" implementation "com.tngtech.archunit:archunit:1.4.0"
implementation "org.gradle:test-retry-gradle-plugin:1.6.2" implementation "org.gradle:test-retry-gradle-plugin:1.6.2"
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}" implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"

View File

@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class); project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("10.26.1"); checkstyle.setToolVersion("11.0.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();

View File

@ -17,6 +17,10 @@
package org.springframework.build; package org.springframework.build;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.jetbrains.dokka.gradle.DokkaExtension;
import org.jetbrains.dokka.gradle.DokkaPlugin;
import org.jetbrains.kotlin.gradle.dsl.JvmTarget; import org.jetbrains.kotlin.gradle.dsl.JvmTarget;
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion; import org.jetbrains.kotlin.gradle.dsl.KotlinVersion;
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile; import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
@ -28,8 +32,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
public class KotlinConventions { public class KotlinConventions {
void apply(Project project) { void apply(Project project) {
project.getPlugins().withId("org.jetbrains.kotlin.jvm", project.getPlugins().withId("org.jetbrains.kotlin.jvm", plugin -> {
(plugin) -> project.getTasks().withType(KotlinCompile.class, this::configure)); project.getTasks().withType(KotlinCompile.class, this::configure);
if (project.getLayout().getProjectDirectory().dir("src/main/kotlin").getAsFile().exists()) {
project.getPlugins().apply(DokkaPlugin.class);
project.getExtensions().configure(DokkaExtension.class, dokka -> configure(project, dokka));
project.project(":framework-api").getDependencies().add("dokka", project);
}
});
} }
private void configure(KotlinCompile compile) { private void configure(KotlinCompile compile) {
@ -49,4 +59,35 @@ public class KotlinConventions {
}); });
} }
private void configure(Project project, DokkaExtension dokka) {
dokka.getDokkaSourceSets().forEach(sourceSet -> {
sourceSet.getSourceRoots().setFrom(project.file("src/main/kotlin"));
sourceSet.getClasspath()
.from(project.getExtensions()
.getByType(SourceSetContainer.class)
.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
.getOutput());
var externalDocumentationLinks = sourceSet.getExternalDocumentationLinks();
var springVersion = project.getVersion();
externalDocumentationLinks.register("spring-framework", spec -> {
spec.url("https://docs.spring.io/spring-framework/docs/" + springVersion + "/javadoc-api/");
spec.packageListUrl("https://docs.spring.io/spring-framework/docs/" + springVersion + "/javadoc-api/element-list");
});
externalDocumentationLinks.register("reactor-core", spec ->
spec.url("https://projectreactor.io/docs/core/release/api/"));
externalDocumentationLinks.register("reactive-streams", spec ->
spec.url("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/"));
externalDocumentationLinks.register("kotlinx-coroutines", spec ->
spec.url("https://kotlinlang.org/api/kotlinx.coroutines/"));
externalDocumentationLinks.register("hamcrest", spec ->
spec.url("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/"));
externalDocumentationLinks.register("jakarta-servlet", spec -> {
spec.url("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/");
spec.packageListUrl("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list");
});
externalDocumentationLinks.register("rsocket-core", spec ->
spec.url("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/"));
});
}
} }

View File

@ -16,9 +16,9 @@
package org.springframework.build; package org.springframework.build;
import java.util.Map;
import org.gradle.api.Project; 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.JavaBasePlugin;
import org.gradle.api.tasks.testing.Test; import org.gradle.api.tasks.testing.Test;
import org.gradle.api.tasks.testing.TestFrameworkOptions; import org.gradle.api.tasks.testing.TestFrameworkOptions;
@ -26,12 +26,16 @@ import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions;
import org.gradle.testretry.TestRetryPlugin; import org.gradle.testretry.TestRetryPlugin;
import org.gradle.testretry.TestRetryTaskExtension; import org.gradle.testretry.TestRetryTaskExtension;
import java.util.Map;
/** /**
* Conventions that are applied in the presence of the {@link JavaBasePlugin}. When the * Conventions that are applied in the presence of the {@link JavaBasePlugin}. When the
* plugin is applied: * plugin is applied:
* <ul> * <ul>
* <li>The {@link TestRetryPlugin Test Retry} plugin is applied so that flaky tests * <li>The {@link TestRetryPlugin Test Retry} plugin is applied so that flaky tests
* are retried 3 times when running on the CI server. * are retried 3 times when running on the CI server.
* <li>Common test properties are configured
* <li>The ByteBuddy Java agent is configured on test tasks.
* </ul> * </ul>
* *
* @author Brian Clozel * @author Brian Clozel
@ -45,6 +49,7 @@ class TestConventions {
} }
private void configureTestConventions(Project project) { private void configureTestConventions(Project project) {
configureByteBuddyAgent(project);
project.getTasks().withType(Test.class, project.getTasks().withType(Test.class,
test -> { test -> {
configureTests(project, test); configureTests(project, test);
@ -75,6 +80,20 @@ class TestConventions {
); );
} }
private void configureByteBuddyAgent(Project project) {
if (project.hasProperty("byteBuddyVersion")) {
String byteBuddyVersion = (String) project.getProperties().get("byteBuddyVersion");
Configuration byteBuddyAgentConfig = project.getConfigurations().create("byteBuddyAgentConfig");
byteBuddyAgentConfig.setTransitive(false);
Dependency byteBuddyAgent = project.getDependencies().create("net.bytebuddy:byte-buddy-agent:" + byteBuddyVersion);
byteBuddyAgentConfig.getDependencies().add(byteBuddyAgent);
project.afterEvaluate(p -> {
p.getTasks().withType(Test.class, test -> test
.jvmArgs("-javaagent:" + byteBuddyAgentConfig.getAsPath()));
});
}
}
private void configureTestRetryPlugin(Project project, Test test) { private void configureTestRetryPlugin(Project project, Test test) {
project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> { project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> {
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class); TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);

View File

@ -1,6 +1,7 @@
plugins { plugins {
id 'java-platform' id 'java-platform'
id 'io.freefair.aggregate-javadoc' version '8.13.1' id 'io.freefair.aggregate-javadoc' version '8.13.1'
id 'org.jetbrains.dokka'
} }
description = "Spring Framework API Docs" description = "Spring Framework API Docs"
@ -54,23 +55,19 @@ javadoc {
} }
} }
/** dokka {
* Produce KDoc for all Spring Framework modules in "build/docs/kdoc" moduleName = "spring-framework"
*/ dokkaPublications.html {
rootProject.tasks.dokkaHtmlMultiModule.configure { outputDirectory = project.java.docsDir.dir("kdoc-api")
dependsOn { includes.from("$rootProject.rootDir/framework-docs/src/docs/api/dokka-overview.md")
tasks.named("javadoc")
} }
moduleName.set("spring-framework")
outputDirectory.set(project.java.docsDir.dir("kdoc-api").get().asFile)
includes.from("$rootProject.rootDir/framework-docs/src/docs/api/dokka-overview.md")
} }
/** /**
* Zip all Java docs (javadoc & kdoc) into a single archive * Zip all Java docs (javadoc & kdoc) into a single archive
*/ */
tasks.register('docsZip', Zip) { tasks.register('docsZip', Zip) {
dependsOn = ['javadoc', rootProject.tasks.dokkaHtmlMultiModule] dependsOn = ['javadoc', 'dokkaGenerate']
group = "distribution" group = "distribution"
description = "Builds -${archiveClassifier} archive containing api and reference " + description = "Builds -${archiveClassifier} archive containing api and reference " +
"for deployment at https://docs.spring.io/spring-framework/docs/." "for deployment at https://docs.spring.io/spring-framework/docs/."
@ -83,7 +80,7 @@ tasks.register('docsZip', Zip) {
from(javadoc) { from(javadoc) {
into "javadoc-api" into "javadoc-api"
} }
from(rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) { from(project.java.docsDir.dir("kdoc-api")) {
into "kdoc-api" into "kdoc-api"
} }
} }

View File

@ -197,6 +197,7 @@
*** xref:web/webmvc-functional.adoc[] *** xref:web/webmvc-functional.adoc[]
*** xref:web/webmvc/mvc-uri-building.adoc[] *** xref:web/webmvc/mvc-uri-building.adoc[]
*** xref:web/webmvc/mvc-ann-async.adoc[] *** xref:web/webmvc/mvc-ann-async.adoc[]
*** xref:web/webmvc/mvc-range.adoc[]
*** xref:web/webmvc-cors.adoc[] *** xref:web/webmvc-cors.adoc[]
*** xref:web/webmvc-versioning.adoc[] *** xref:web/webmvc-versioning.adoc[]
*** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[] *** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[]
@ -294,6 +295,7 @@
**** xref:web/webflux/controller/ann-advice.adoc[] **** xref:web/webflux/controller/ann-advice.adoc[]
*** xref:web/webflux-functional.adoc[] *** xref:web/webflux-functional.adoc[]
*** xref:web/webflux/uri-building.adoc[] *** xref:web/webflux/uri-building.adoc[]
*** xref:web/webflux/range.adoc[]
*** xref:web/webflux-cors.adoc[] *** xref:web/webflux-cors.adoc[]
*** xref:web/webflux-versioning.adoc[] *** xref:web/webflux-versioning.adoc[]
*** xref:web/webflux/ann-rest-exceptions.adoc[] *** xref:web/webflux/ann-rest-exceptions.adoc[]
@ -353,6 +355,7 @@
*** xref:testing/testcontext-framework/support-classes.adoc[] *** xref:testing/testcontext-framework/support-classes.adoc[]
*** xref:testing/testcontext-framework/aot.adoc[] *** xref:testing/testcontext-framework/aot.adoc[]
** xref:testing/webtestclient.adoc[] ** xref:testing/webtestclient.adoc[]
** xref:testing/resttestclient.adoc[]
** xref:testing/mockmvc.adoc[] ** xref:testing/mockmvc.adoc[]
*** xref:testing/mockmvc/overview.adoc[] *** xref:testing/mockmvc/overview.adoc[]
*** xref:testing/mockmvc/setup-options.adoc[] *** xref:testing/mockmvc/setup-options.adoc[]

View File

@ -28,6 +28,10 @@ you can do so. However, you should consider the following issues:
deploying on the module path. Such cases require a JVM bootstrap flag deploying on the module path. Such cases require a JVM bootstrap flag
`--add-opens=java.base/java.lang=ALL-UNNAMED` which is not available for modules. `--add-opens=java.base/java.lang=ALL-UNNAMED` which is not available for modules.
[[aop-forcing-proxy-types]]
== Forcing Specific AOP Proxy Types
To force the use of CGLIB proxies, set the value of the `proxy-target-class` attribute To force the use of CGLIB proxies, set the value of the `proxy-target-class` attribute
of the `<aop:config>` element to true, as follows: of the `<aop:config>` element to true, as follows:
@ -60,6 +64,24 @@ To be clear, using `proxy-target-class="true"` on `<tx:annotation-driven/>`,
proxies _for all three of them_. proxies _for all three of them_.
==== ====
`@EnableAspectJAutoProxy`, `@EnableTransactionManagement` and related configuration
annotations offer a corresponding `proxyTargetClass` attribute. These are collapsed
into a single unified auto-proxy creator too, effectively applying the _strongest_
proxy settings at runtime. As of 7.0, this applies to individual proxy processors
as well, for example `@EnableAsync`, consistently participating in unified global
default settings for all auto-proxying attempts in a given application.
The global default proxy type may differ between setups. While the core framework
suggests interface-based proxies by default, Spring Boot may - depending on
configuration properties - enable class-based proxies by default.
As of 7.0, forcing a specific proxy type for individual beans is possible through
the `@Proxyable` annotation on a given `@Bean` method or `@Component` class, with
`@Proxyable(INTERFACES)` or `@Proxyable(TARGET_CLASS)` overriding any globally
configured default. For very specific purposes, you may even specify the proxy
interface(s) to use through `@Proxyable(interfaces=...)`, limiting the exposure
to selected interfaces rather than all interfaces that the target bean implements.
[[aop-understanding-aop-proxies]] [[aop-understanding-aop-proxies]]
== Understanding AOP Proxies == Understanding AOP Proxies

View File

@ -1,24 +1,26 @@
[[beans-classpath-scanning]] [[beans-classpath-scanning]]
= Classpath Scanning and Managed Components = Classpath Scanning and Managed Components
Most examples in this chapter use XML to specify the configuration metadata that produces Most examples in this chapter use XML to specify the configuration metadata that
each `BeanDefinition` within the Spring container. The previous section produces each `BeanDefinition` within the Spring container. The previous section
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration]) demonstrates how to provide a lot of the configuration (xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration])
metadata through source-level annotations. Even in those examples, however, the "base" demonstrates how to provide a lot of the configuration metadata through source-level
bean definitions are explicitly defined in the XML file, while the annotations drive only annotations. Even in those examples, however, the "base" bean definitions are explicitly
the dependency injection. This section describes an option for implicitly detecting the defined in the XML file, while the annotations drive only the dependency injection.
candidate components by scanning the classpath. Candidate components are classes that
match against a filter criteria and have a corresponding bean definition registered with This section describes an option for implicitly detecting the candidate components by
the container. This removes the need to use XML to perform bean registration. Instead, you scanning the classpath. Candidate components are classes that match against a filter
can use annotations (for example, `@Component`), AspectJ type expressions, or your own criteria and have a corresponding bean definition registered with the container.
This removes the need to use XML to perform bean registration. Instead, you can use
annotations (for example, `@Component`), AspectJ type expressions, or your own
custom filter criteria to select which classes have bean definitions registered with custom filter criteria to select which classes have bean definitions registered with
the container. the container.
[NOTE] [NOTE]
==== ====
You can define beans using Java rather than using XML files. Take a look at the You can define beans using Java rather than using XML files. Take a look at the
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples of how to `@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples
use these features. of how to use these features.
==== ====
@ -828,10 +830,10 @@ definitions, there is no notion of bean definition inheritance, and inheritance
hierarchies at the class level are irrelevant for metadata purposes. hierarchies at the class level are irrelevant for metadata purposes.
For details on web-specific scopes such as "`request`" or "`session`" in a Spring context, For details on web-specific scopes such as "`request`" or "`session`" in a Spring context,
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes]. As with the pre-built annotations for those scopes, see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes].
you may also compose your own scoping annotations by using Spring's meta-annotation As with the pre-built annotations for those scopes, you may also compose your own scoping
approach: for example, a custom annotation meta-annotated with `@Scope("prototype")`, annotations by using Spring's meta-annotation approach: for example, a custom annotation
possibly also declaring a custom scoped-proxy mode. meta-annotated with `@Scope("prototype")`, possibly also declaring a custom scoped-proxy mode.
NOTE: To provide a custom strategy for scope resolution rather than relying on the NOTE: To provide a custom strategy for scope resolution rather than relying on the
annotation-based approach, you can implement the annotation-based approach, you can implement the
@ -873,7 +875,8 @@ Kotlin::
---- ----
When using certain non-singleton scopes, it may be necessary to generate proxies for the When using certain non-singleton scopes, it may be necessary to generate proxies for the
scoped objects. The reasoning is described in xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies]. scoped objects. The reasoning is described in
xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
For this purpose, a scoped-proxy attribute is available on the component-scan For this purpose, a scoped-proxy attribute is available on the component-scan
element. The three possible values are: `no`, `interfaces`, and `targetClass`. For example, element. The three possible values are: `no`, `interfaces`, and `targetClass`. For example,
the following configuration results in standard JDK dynamic proxies: the following configuration results in standard JDK dynamic proxies:

View File

@ -933,13 +933,12 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
// create a startup step and start recording // create a startup step and start recording
StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan"); try (StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")) {
// add tagging information to the current step // add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages)); scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting // perform the actual phase we're instrumenting
this.scanner.scan(basePackages); this.scanner.scan(basePackages);
// end the current step }
scanPackages.end();
---- ----
Kotlin:: Kotlin::
@ -947,13 +946,12 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
// create a startup step and start recording // create a startup step and start recording
val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan") try (val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")) {
// add tagging information to the current step // add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages)) scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting // perform the actual phase we're instrumenting
this.scanner.scan(basePackages) this.scanner.scan(basePackages);
// end the current step }
scanPackages.end()
---- ----
====== ======

View File

@ -3,8 +3,8 @@
Java NIO provides `ByteBuffer` but many libraries build their own byte buffer API on top, Java NIO provides `ByteBuffer` but many libraries build their own byte buffer API on top,
especially for network operations where reusing buffers and/or using direct buffers is especially for network operations where reusing buffers and/or using direct buffers is
beneficial for performance. For example Netty has the `ByteBuf` hierarchy, Undertow uses beneficial for performance. For example Netty has the `ByteBuf` hierarchy,
XNIO, Jetty uses pooled byte buffers with a callback to be released, and so on. Jetty uses pooled byte buffers with a callback to be released, and so on.
The `spring-core` module provides a set of abstractions to work with various byte buffer The `spring-core` module provides a set of abstractions to work with various byte buffer
APIs as follows: APIs as follows:

View File

@ -10,11 +10,11 @@ cover the other ORM technologies and show brief examples.
[NOTE] [NOTE]
==== ====
As of Spring Framework 7.0, Spring requires Hibernate ORM 7.0 for Spring's As of Spring Framework 7.0, Spring requires Hibernate ORM 7.x for Spring's
`HibernateJpaVendorAdapter` as well as for a native Hibernate `SessionFactory` setup. `HibernateJpaVendorAdapter`.
The `org.springframework.orm.jpa.hibernate` package supersedes the former `orm.hibernate5`: The `org.springframework.orm.jpa.hibernate` package supersedes the former `orm.hibernate5`:
now for use with Hibernate ORM 7.0, tightly integrated with `HibernateJpaVendorAdapter` now for use with Hibernate ORM 7.1+, tightly integrated with `HibernateJpaVendorAdapter`
as well as supporting Hibernate's native `SessionFactory.getCurrentSession()` style. as well as supporting Hibernate's native `SessionFactory.getCurrentSession()` style.
==== ====

View File

@ -86,11 +86,13 @@ rollback rules may be configured via the `rollbackFor`/`noRollbackFor` and
`rollbackForClassName`/`noRollbackForClassName` attributes, which allow rules to be `rollbackForClassName`/`noRollbackForClassName` attributes, which allow rules to be
defined based on exception types or patterns, respectively. defined based on exception types or patterns, respectively.
When a rollback rule is defined with an exception type, that type will be used to match When a rollback rule is defined with an exception type for example, via `rollbackFor`
against the type of a thrown exception and its super types, providing type safety and that type will be used to match against the type of a thrown exception. Specifically,
avoiding any unintentional matches that may occur when using a pattern. For example, a given a configured exception type `C`, a thrown exception of type `T` will be considered
value of `jakarta.servlet.ServletException.class` will only match thrown exceptions of a match against `C` if `T` is equal to `C` or a subclass of `C`. This provides type
type `jakarta.servlet.ServletException` and its subclasses. safety and avoids any unintentional matches that may occur when using a pattern. For
example, a value of `jakarta.servlet.ServletException.class` will only match thrown
exceptions of type `jakarta.servlet.ServletException` and its subclasses.
When a rollback rule is defined with an exception pattern, the pattern can be a fully When a rollback rule is defined with an exception pattern, the pattern can be a fully
qualified class name or a substring of a fully qualified class name for an exception type qualified class name or a substring of a fully qualified class name for an exception type

View File

@ -9,39 +9,7 @@ that takes no destination argument uses the default destination.
The following example uses the `MessageCreator` callback to create a text message from the The following example uses the `MessageCreator` callback to create a text message from the
supplied `Session` object: supplied `Session` object:
[source,java,indent=0,subs="verbatim,quotes"] include-code::./JmsQueueSender[]
----
import jakarta.jms.ConnectionFactory;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.Queue;
import jakarta.jms.Session;
import org.springframework.jms.core.MessageCreator;
import org.springframework.jms.core.JmsTemplate;
public class JmsQueueSender {
private JmsTemplate jmsTemplate;
private Queue queue;
public void setConnectionFactory(ConnectionFactory cf) {
this.jmsTemplate = new JmsTemplate(cf);
}
public void setQueue(Queue queue) {
this.queue = queue;
}
public void simpleSend() {
this.jmsTemplate.send(this.queue, new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("hello queue world");
}
});
}
}
----
In the preceding example, the `JmsTemplate` is constructed by passing a reference to a In the preceding example, the `JmsTemplate` is constructed by passing a reference to a
`ConnectionFactory`. As an alternative, a zero-argument constructor and `ConnectionFactory`. As an alternative, a zero-argument constructor and
@ -84,21 +52,7 @@ gives you access to the message after it has been converted but before it is sen
following example shows how to modify a message header and a property after a following example shows how to modify a message header and a property after a
`java.util.Map` is converted to a message: `java.util.Map` is converted to a message:
[source,java,indent=0,subs="verbatim,quotes"] include-code::./JmsSenderWithConversion[]
----
public void sendWithConversion() {
Map<String, String> map = new HashMap<>();
map.put("Name", "Mark");
map.put("Age", new Integer(47));
jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() {
public Message postProcessMessage(Message message) throws JMSException {
message.setIntProperty("AccountID", 1234);
message.setJMSCorrelationID("123-00001");
return message;
}
});
}
----
This results in a message of the following form: This results in a message of the following form:
@ -126,32 +80,6 @@ to `jakarta.jms.TextMessage`, `jakarta.jms.BytesMessage`, etc. For a contract su
generic message payloads, use `org.springframework.messaging.converter.MessageConverter` generic message payloads, use `org.springframework.messaging.converter.MessageConverter`
with `JmsMessagingTemplate` or preferably `JmsClient` as your central delegate instead. with `JmsMessagingTemplate` or preferably `JmsClient` as your central delegate instead.
[[jms-sending-jmsclient]]
== Sending a Message with `JmsClient`
[source,java,indent=0,subs="verbatim,quotes"]
----
// Reusable handle, typically created through JmsClient.create(ConnectionFactory)
// For custom conversion, use JmsClient.create(ConnectionFactory, MessageConverter)
private JmsClient jmsClient;
public void sendWithConversion() {
this.jmsClient.destination("myQueue")
.withTimeToLive(1000)
.send("myPayload"); // optionally with a headers Map next to the payload
}
public void sendCustomMessage() {
Message<?> message =
MessageBuilder.withPayload("myPayload").build(); // optionally with headers
this.jmsClient.destination("myQueue")
.withTimeToLive(1000)
.send(message);
}
----
[[jms-sending-callbacks]] [[jms-sending-callbacks]]
== Using `SessionCallback` and `ProducerCallback` on `JmsTemplate` == Using `SessionCallback` and `ProducerCallback` on `JmsTemplate`
@ -160,3 +88,21 @@ want to perform multiple operations on a JMS `Session` or `MessageProducer`. The
`SessionCallback` and `ProducerCallback` expose the JMS `Session` and `Session` / `SessionCallback` and `ProducerCallback` expose the JMS `Session` and `Session` /
`MessageProducer` pair, respectively. The `execute()` methods on `JmsTemplate` run `MessageProducer` pair, respectively. The `execute()` methods on `JmsTemplate` run
these callback methods. these callback methods.
[[jms-sending-jmsclient]]
== Sending a Message with `JmsClient`
include-code::./JmsClientSample[]
[[jms-sending-postprocessor]]
== Post-processing outgoing messages
Applications often need to intercept messages before they are sent out, for example to add message properties to all outgoing messages.
The `org.springframework.messaging.core.MessagePostProcessor` based on the spring-messaging `Message` can do that,
when configured on the `JmsClient`. It will be used for all outgoing messages sent with the `send` and `sendAndReceive` methods.
Here is an example of an interceptor adding a "tenantId" property to all outgoing messages.
include-code::./JmsClientWithPostProcessor[]

View File

@ -189,13 +189,13 @@ This observation uses the `io.micrometer.jakarta9.instrument.jms.DefaultJmsProce
[[observability.http-server]] [[observability.http-server]]
== HTTP Server instrumentation == HTTP Server instrumentation
HTTP server exchange observations are created with the name `"http.server.requests"` for Servlet and Reactive applications. HTTP server exchange observations are created with the name `"http.server.requests"` for Servlet and Reactive applications,
or "http.server.request.duration" if using the OpenTelemetry convention.
[[observability.http-server.servlet]] [[observability.http-server.servlet]]
=== Servlet applications === Servlet applications
Applications need to configure the `org.springframework.web.filter.ServerHttpObservationFilter` Servlet filter in their application. Applications need to configure the `org.springframework.web.filter.ServerHttpObservationFilter` Servlet filter in their application.
It uses the `org.springframework.http.server.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
This will only record an observation as an error if the `Exception` has not been handled by the web framework and has bubbled up to the Servlet filter. This will only record an observation as an error if the `Exception` has not been handled by the web framework and has bubbled up to the Servlet filter.
Typically, all exceptions handled by Spring MVC's `@ExceptionHandler` and xref:web/webmvc/mvc-ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation. Typically, all exceptions handled by Spring MVC's `@ExceptionHandler` and xref:web/webmvc/mvc-ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation.
@ -207,6 +207,11 @@ NOTE: Because the instrumentation is done at the Servlet Filter level, the obser
Typically, Servlet container error handling is performed at a lower level and won't have any active observation or span. Typically, Servlet container error handling is performed at a lower level and won't have any active observation or span.
For this use case, a container-specific implementation is required, such as a `org.apache.catalina.Valve` for Tomcat; this is outside the scope of this project. For this use case, a container-specific implementation is required, such as a `org.apache.catalina.Valve` for Tomcat; this is outside the scope of this project.
[[observability.http-server.servlet.default]]
==== Default Semantic Convention
It uses the `org.springframework.http.server.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
By default, the following `KeyValues` are created: By default, the following `KeyValues` are created:
.Low cardinality Keys .Low cardinality Keys
@ -228,6 +233,16 @@ By default, the following `KeyValues` are created:
|`http.url` _(required)_|HTTP request URI. |`http.url` _(required)_|HTTP request URI.
|=== |===
[[observability.http-server.servlet.otel]]
==== OpenTelemetry Semantic Convention
An OpenTelemetry variant is available with `org.springframework.http.server.observation.OpenTelemetryServerRequestObservationConvention`, backed by the `ServerRequestObservationContext`.
This variant complies with the https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/http/http-metrics.md[OpenTelemetry Semantic Conventions for HTTP Metrics (v1.36.0)]
and the https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/http/http-spans.md[OpenTelemetry Semantic Conventions for HTTP Spans (v1.36.0)].
[[observability.http-server.reactive]] [[observability.http-server.reactive]]
=== Reactive applications === Reactive applications

View File

@ -1195,6 +1195,46 @@ One way to declare HTTP Service groups is via `@ImportHttpServices` annotations
<1> Manually list interfaces for group "echo" <1> Manually list interfaces for group "echo"
<2> Detect interfaces for group "greeting" under a base package <2> Detect interfaces for group "greeting" under a base package
The above lets you declare HTTP Services and groups. As an alternative, you can also
annotate HTTP interfaces as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@HttpServiceClient("echo")
public interface EchoServiceA {
// ...
}
@HttpServiceClient("echo")
public interface EchoServiceB {
// ...
}
----
The above requires a dedicated import registrar as follows:
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyClientHttpServiceRegistrar implements AbstractClientHttpServiceRegistrar { // <1>
@Override
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
findAndRegisterHttpServiceClients(groupRegistry, List.of("org.example.echo")); // <2>
}
}
@Configuration
@Import(MyClientHttpServiceRegistrar.class) // <3>
public class ClientConfig {
}
----
<1> Extend dedicated `AbstractClientHttpServiceRegistrar`
<2> Specify base packages where to find client interfaces
<3> Import the registrar
TIP: `@HttpServiceClient` interfaces are excluded from `@ImportHttpServices` scans, so there
is no overlap with scans for client interfaces when pointed at the same package.
It is also possible to declare groups programmatically by creating an HTTP Service It is also possible to declare groups programmatically by creating an HTTP Service
registrar and then importing it: registrar and then importing it:

View File

@ -73,7 +73,7 @@ As of Spring Framework 6.0, Spring has been upgraded to the Jakarta EE 9 level
traditional `javax` packages. With EE 9 as the minimum and EE 10 supported already, traditional `javax` packages. With EE 9 as the minimum and EE 10 supported already,
Spring is prepared to provide out-of-the-box support for the further evolution of Spring is prepared to provide out-of-the-box support for the further evolution of
the Jakarta EE APIs. Spring Framework 6.0 is fully compatible with Tomcat 10.1, the Jakarta EE APIs. Spring Framework 6.0 is fully compatible with Tomcat 10.1,
Jetty 11 and Undertow 2.3 as web servers, and also with Hibernate ORM 6.1. Jetty 11 as web servers, and also with Hibernate ORM 6.1.
Over time, the role of Java/Jakarta EE in application development has evolved. In the Over time, the role of Java/Jakarta EE in application development has evolved. In the
early days of J2EE and Spring, applications were created to be deployed to an application early days of J2EE and Spring, applications were created to be deployed to an application

View File

@ -261,7 +261,7 @@ Kotlin::
This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test]
by leveraging the Page Object Pattern. As we mentioned in by leveraging the Page Object Pattern. As we mentioned in
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
with HtmlUnit, but it is much easier with WebDriver. Consider the following with HtmlUnit, but it is much easier with WebDriver. Consider the following
`CreateMessagePage` implementation: `CreateMessagePage` implementation:

View File

@ -0,0 +1,434 @@
[[resttestclient]]
= RestTestClient
`RestTestClient` is an HTTP client designed for testing server applications. It wraps
Spring's xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and uses it to perform requests,
but exposes a testing facade for verifying responses. `RestTestClient` can be used to
perform end-to-end HTTP tests. It can also be used to test Spring MVC
applications without a running server via MockMvc.
[[resttestclient-setup]]
== Setup
To set up a `RestTestClient` you need to choose a server setup to bind to. This can be one
of several MockMvc setup choices, or a connection to a live server.
[[resttestclient-controller-config]]
=== Bind to Controller
This setup allows you to test specific controller(s) via mock request and response objects,
without a running server.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
RestTestClient client =
RestTestClient.bindToController(new TestController()).build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val client = RestTestClient.bindToController(TestController()).build()
----
======
[[resttestclient-context-config]]
=== Bind to `ApplicationContext`
This setup allows you to load Spring configuration with Spring MVC
infrastructure and controller declarations and use it to handle requests via mock request
and response objects, without a running server.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(WebConfig.class) // <1>
class MyTests {
RestTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { // <2>
client = RestTestClient.bindToApplicationContext(context).build(); // <3>
}
}
----
<1> Specify the configuration to load
<2> Inject the configuration
<3> Create the `RestTestClient`
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(WebConfig::class) // <1>
class MyTests {
lateinit var client: RestTestClient
@BeforeEach
fun setUp(context: ApplicationContext) { // <2>
client = RestTestClient.bindToApplicationContext(context).build() // <3>
}
}
----
<1> Specify the configuration to load
<2> Inject the configuration
<3> Create the `RestTestClient`
======
[[resttestclient-fn-config]]
=== Bind to Router Function
This setup allows you to test xref:web/webmvc-functional.adoc[functional endpoints] via
mock request and response objects, without a running server.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
RouterFunction<?> route = ...
client = RestTestClient.bindToRouterFunction(route).build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val route: RouterFunction<*> = ...
val client = RestTestClient.bindToRouterFunction(route).build()
----
======
[[resttestclient-server-config]]
=== Bind to Server
This setup connects to a running server to perform full, end-to-end HTTP tests:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build()
----
======
[[resttestclient-client-config]]
=== Client Config
In addition to the server setup options described earlier, you can also configure client
options, including base URL, default headers, client filters, and others. These options
are readily available following the initial `bindTo` call, as follows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client = RestTestClient.bindToController(new TestController())
.baseUrl("/test")
.build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client = RestTestClient.bindToController(TestController())
.baseUrl("/test")
.build()
----
======
[[resttestclient-tests]]
== Writing Tests
`RestTestClient` provides an API identical to xref:integration/rest-clients.adoc#rest-restclient[`RestClient`]
up to the point of performing a request by using `exchange()`.
After the call to `exchange()`, `RestTestClient` diverges from `RestClient`, and
instead continues with a workflow to verify responses.
To assert the response status and headers, use the following:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
----
======
If you would like for all expectations to be asserted even if one of them fails, you can
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in
JUnit Jupiter.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
spec -> spec.expectStatus().isOk(),
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
{ spec -> spec.expectStatus().isOk() },
{ spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) }
)
----
======
You can then choose to decode the response body through one of the following:
* `expectBody(Class<T>)`: Decode to single object.
* `expectBody()`: Decode to `byte[]` for xref:testing/resttestclient.adoc#resttestclient-json[JSON Content] or an empty body.
If the built-in assertions are insufficient, you can consume the object instead and
perform any other assertions:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (for example, AssertJ)...
});
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody<Person>()
.consumeWith {
// custom assertions (for example, AssertJ)...
}
----
======
Or you can exit the workflow and obtain a `EntityExchangeResult`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk
.expectBody<Person>()
.returnResult()
----
======
TIP: When you need to decode to a target type with generics, look for the overloaded methods
that accept {spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`]
instead of `Class<T>`.
[[resttestclient-no-content]]
=== No Content
If the response is not expected to have content, you can assert that as follows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty()
----
======
If you want to ignore the response content, the following releases the content without any assertions:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound
.expectBody<Unit>()
----
======
[[resttestclient-json]]
=== JSON Content
You can use `expectBody()` without a target type to perform assertions on the raw
content rather than through higher level Object(s).
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
----
======
To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
----
======

View File

@ -2,10 +2,10 @@
= Application Events = Application Events
The TestContext framework provides support for recording The TestContext framework provides support for recording
xref:core/beans/context-introduction.adoc#context-functionality-events[application events] published in the xref:core/beans/context-introduction.adoc#context-functionality-events[application events]
`ApplicationContext` so that assertions can be performed against those events within published in the `ApplicationContext` so that assertions can be performed against those
tests. All events published during the execution of a single test are made available via events within tests. All events published during the execution of a single test are made
the `ApplicationEvents` API which allows you to process the events as a available via the `ApplicationEvents` API which allows you to process the events as a
`java.util.Stream`. `java.util.Stream`.
To use `ApplicationEvents` in your tests, do the following. To use `ApplicationEvents` in your tests, do the following.
@ -16,16 +16,23 @@ To use `ApplicationEvents` in your tests, do the following.
that `ApplicationEventsTestExecutionListener` is registered by default and only needs that `ApplicationEventsTestExecutionListener` is registered by default and only needs
to be manually registered if you have custom configuration via to be manually registered if you have custom configuration via
`@TestExecutionListeners` that does not include the default listeners. `@TestExecutionListeners` that does not include the default listeners.
* Annotate a field of type `ApplicationEvents` with `@Autowired` and use that instance of * When using the
`ApplicationEvents` in your test and lifecycle methods (such as `@BeforeEach` and xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter],
`@AfterEach` methods in JUnit Jupiter). declare a method parameter of type `ApplicationEvents` in a `@Test`, `@BeforeEach`, or
** When using the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter], you may declare a method `@AfterEach` method.
parameter of type `ApplicationEvents` in a test or lifecycle method as an alternative ** Since `ApplicationEvents` is scoped to the lifecycle of the current test method, this
to an `@Autowired` field in the test class. is the recommended approach.
* Alternatively, you can annotate a field of type `ApplicationEvents` with `@Autowired`
and use that instance of `ApplicationEvents` in your test and lifecycle methods.
NOTE: `ApplicationEvents` is registered with the `ApplicationContext` as a _resolvable
dependency_ which is scoped to the lifecycle of the current test method. Consequently,
`ApplicationEvents` cannot be accessed outside the lifecycle of a test method and cannot be
`@Autowired` into the constructor of a test class.
The following test class uses the `SpringExtension` for JUnit Jupiter and The following test class uses the `SpringExtension` for JUnit Jupiter and
{assertj-docs}[AssertJ] to assert the types of application events {assertj-docs}[AssertJ] to assert the types of application events published while
published while invoking a method in a Spring-managed component: invoking a method in a Spring-managed component:
// Don't use "quotes" in the "subs" section because of the asterisks in /* ... */ // Don't use "quotes" in the "subs" section because of the asterisks in /* ... */
[tabs] [tabs]
@ -38,16 +45,10 @@ Java::
@RecordApplicationEvents // <1> @RecordApplicationEvents // <1>
class OrderServiceTests { class OrderServiceTests {
@Autowired
OrderService orderService;
@Autowired
ApplicationEvents events; // <2>
@Test @Test
void submitOrder() { void submitOrder(@Autowired OrderService service, ApplicationEvents events) { // <2>
// Invoke method in OrderService that publishes an event // Invoke method in OrderService that publishes an event
orderService.submitOrder(new Order(/* ... */)); service.submitOrder(new Order(/* ... */));
// Verify that an OrderSubmitted event was published // Verify that an OrderSubmitted event was published
long numEvents = events.stream(OrderSubmitted.class).count(); // <3> long numEvents = events.stream(OrderSubmitted.class).count(); // <3>
assertThat(numEvents).isEqualTo(1); assertThat(numEvents).isEqualTo(1);
@ -66,16 +67,10 @@ Kotlin::
@RecordApplicationEvents // <1> @RecordApplicationEvents // <1>
class OrderServiceTests { class OrderServiceTests {
@Autowired
lateinit var orderService: OrderService
@Autowired
lateinit var events: ApplicationEvents // <2>
@Test @Test
fun submitOrder() { fun submitOrder(@Autowired service: OrderService, events: ApplicationEvents) { // <2>
// Invoke method in OrderService that publishes an event // Invoke method in OrderService that publishes an event
orderService.submitOrder(Order(/* ... */)) service.submitOrder(Order(/* ... */))
// Verify that an OrderSubmitted event was published // Verify that an OrderSubmitted event was published
val numEvents = events.stream(OrderSubmitted::class).count() // <3> val numEvents = events.stream(OrderSubmitted::class).count() // <3>
assertThat(numEvents).isEqualTo(1) assertThat(numEvents).isEqualTo(1)

View File

@ -63,13 +63,15 @@ alternative, you can set the same property via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
As of Spring Framework 7.0, an application context stored in the context cache will be As of Spring Framework 7.0, an application context stored in the context cache will be
stopped when it is no longer actively in use and automatically restarted the next time _paused_ when it is no longer actively in use and automatically _restarted_ the next time
the context is retrieved from the cache. Specifically, the latter will restart all the context is retrieved from the cache. Specifically, the latter will restart all
auto-startup beans in the application context, effectively restoring the lifecycle state. auto-startup beans in the application context, effectively restoring the lifecycle state.
This ensures that background processes within the context are not actively running while This ensures that background processes within the context are not actively running while
the context is not used by tests. For example, JMS listener containers, scheduled tasks, the context is not used by tests. For example, JMS listener containers, scheduled tasks,
and any other components in the context that implement `Lifecycle` or `SmartLifecycle` and any other components in the context that implement `Lifecycle` or `SmartLifecycle`
will be in a "stopped" state until the context is used again by a test. will be in a "stopped" state until the context is used again by a test. Note, however,
that `SmartLifecycle` components can opt out of pausing by returning `false` from
`SmartLifecycle#isPauseable()`.
Since having a large number of application contexts loaded within a given test suite can Since having a large number of application contexts loaded within a given test suite can
cause the suite to take an unnecessarily long time to run, it is often beneficial to cause the suite to take an unnecessarily long time to run, it is often beneficial to

View File

@ -3,7 +3,7 @@
This part of the documentation covers support for reactive-stack web applications built This part of the documentation covers support for reactive-stack web applications built
on a {reactive-streams-site}/[Reactive Streams] API to run on non-blocking servers, on a {reactive-streams-site}/[Reactive Streams] API to run on non-blocking servers,
such as Netty, Undertow, and Servlet containers. Individual chapters cover such as Netty and Servlet containers. Individual chapters cover
the xref:web/webflux.adoc#webflux[Spring WebFlux] framework, the xref:web/webflux.adoc#webflux[Spring WebFlux] framework,
the reactive xref:web/webflux-webclient.adoc[`WebClient`], the reactive xref:web/webflux-webclient.adoc[`WebClient`],
support for xref:web/webflux-test.adoc[testing], support for xref:web/webflux-test.adoc[testing],

View File

@ -46,8 +46,13 @@ directly with it.
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Servlet stack]# [.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Servlet stack]#
This strategy resolves the API version from a request. The WebFlux config provides built-in This strategy resolves the API version from a request. The WebFlux config provides built-in
options to resolve from a header, a request parameter, or from the URL path. options to resolve from a header, query parameter, media type parameter,
You can also use a custom `ApiVersionResolver`. or from the URL path. You can also use a custom `ApiVersionResolver`.
NOTE: The path resolver always resolves the version from the specified path segment, or
raises `InvalidApiVersionException` otherwise, and therefore it cannot yield to other
resolvers.

View File

@ -367,7 +367,7 @@ subsequently use `DataBufferUtils.release(dataBuffer)` when the buffers are cons
`WebSocketHandlerAdapter` delegates to a `WebSocketService`. By default, that is an instance `WebSocketHandlerAdapter` delegates to a `WebSocketService`. By default, that is an instance
of `HandshakeWebSocketService`, which performs basic checks on the WebSocket request and of `HandshakeWebSocketService`, which performs basic checks on the WebSocket request and
then uses `RequestUpgradeStrategy` for the server in use. Currently, there is built-in then uses `RequestUpgradeStrategy` for the server in use. Currently, there is built-in
support for Reactor Netty, Tomcat, Jetty, and Undertow. support for Reactor Netty, Tomcat, and Jetty.
`HandshakeWebSocketService` exposes a `sessionAttributePredicate` property that allows `HandshakeWebSocketService` exposes a `sessionAttributePredicate` property that allows
setting a `Predicate<String>` to extract attributes from the `WebSession` and insert them setting a `Predicate<String>` to extract attributes from the `WebSession` and insert them
@ -446,7 +446,7 @@ specify CORS settings by URL pattern. If both are specified, they are combined b
=== Client === Client
Spring WebFlux provides a `WebSocketClient` abstraction with implementations for Spring WebFlux provides a `WebSocketClient` abstraction with implementations for
Reactor Netty, Tomcat, Jetty, Undertow, and standard Java (that is, JSR-356). Reactor Netty, Tomcat, Jetty, and standard Java (that is, JSR-356).
NOTE: The Tomcat client is effectively an extension of the standard Java one with some extra NOTE: The Tomcat client is effectively an extension of the standard Java one with some extra
functionality in the `WebSocketSession` handling to take advantage of the Tomcat-specific functionality in the `WebSocketSession` handling to take advantage of the Tomcat-specific

View File

@ -8,7 +8,7 @@ The original web framework included in the Spring Framework, Spring Web MVC, was
purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework,
Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports
{reactive-streams-site}/[Reactive Streams] back pressure, and runs on such servers as {reactive-streams-site}/[Reactive Streams] back pressure, and runs on such servers as
Netty, Undertow, and Servlet containers. Netty, and Servlet containers.
Both web frameworks mirror the names of their source modules Both web frameworks mirror the names of their source modules
({spring-framework-code}/spring-webmvc[spring-webmvc] and ({spring-framework-code}/spring-webmvc[spring-webmvc] and

View File

@ -93,6 +93,10 @@ You can map requests by using glob patterns and wildcards:
|=== |===
|Pattern |Description |Example |Pattern |Description |Example
| `spring`
| Literal pattern
| `+"/spring"+` matches `+"/spring"+`
| `+?+` | `+?+`
| Matches one character | Matches one character
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+` | `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
@ -104,23 +108,41 @@ You can map requests by using glob patterns and wildcards:
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+` `+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`
| `+**+` | `+**+`
| Matches zero or more path segments until the end of the path | Matches zero or more path segments
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+` | `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
`+"/resources/**/file.png"+` is invalid as `+**+` is only allowed at the end of the path. `+"/**/resources"+` matches `+"/spring/resources"+` and `+"/spring/framework/resources"+`
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
`+"/**/{name}/resources"+` is invalid as only a literal pattern is allowed right after `+**+`.
`+"/**/project/{project}/resources"+` is allowed.
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
| `+{name}+` | `+{name}+`
| Matches a path segment and captures it as a variable named "name" | Matches a path segment and captures it as a variable named "name"
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+` | `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
| `+{name:[a-z]+}+` | `+{name:[a-z]+}+`
| Matches the regexp `+"[a-z]+"+` as a path variable named "name" | Matches the regexp `+"[a-z]+"+` as a path variable named "name"
| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+` | `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+`
| `+{*path}+` | `+{*path}+`
| Matches zero or more path segments until the end of the path and captures it as a variable named "path" | Matches zero or more path segments and captures it as a variable named "path"
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+` | `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
`+"/{*path}/{name}/resources"+` is invalid as only a literal pattern is allowed right after `{*path}`.
`+"/{*path}/project/{project}/resources"+` is allowed.
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|=== |===
Captured URI variables can be accessed with `@PathVariable`, as the following example shows: Captured URI variables can be accessed with `@PathVariable`, as the following example shows:
@ -234,10 +256,13 @@ Kotlin::
====== ======
-- --
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup URI path patterns can also have:
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this, for example, to parameterize a base URL based on - Embedded `${...}` placeholders that are resolved on startup via
some external configuration. `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. This is useful, for example, to parameterize a base URL based on
external configuration.
- SpEL expressions `#{...}`.
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support. NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
Both classes are located in `spring-web` and are expressly designed for use with HTTP URL Both classes are located in `spring-web` and are expressly designed for use with HTTP URL

View File

@ -4,6 +4,6 @@
[.small]#xref:web/webmvc/mvc-http2.adoc[See equivalent in the Servlet stack]# [.small]#xref:web/webmvc/mvc-http2.adoc[See equivalent in the Servlet stack]#
HTTP/2 is supported with Reactor Netty, Tomcat, Jetty, and Undertow. However, there are HTTP/2 is supported with Reactor Netty, Tomcat, and Jetty. However, there are
considerations related to server configuration. For more details, see the considerations related to server configuration. For more details, see the
{spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page]. {spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page].

View File

@ -127,7 +127,7 @@ You have maximum choice of libraries, since, historically, most are blocking.
* If you are already shopping for a non-blocking web stack, Spring WebFlux offers the same * If you are already shopping for a non-blocking web stack, Spring WebFlux offers the same
execution model benefits as others in this space and also provides a choice of servers execution model benefits as others in this space and also provides a choice of servers
(Netty, Tomcat, Jetty, Undertow, and Servlet containers), a choice of programming models (Netty, Tomcat, Jetty, and Servlet containers), a choice of programming models
(annotated controllers and functional web endpoints), and a choice of reactive libraries (annotated controllers and functional web endpoints), and a choice of reactive libraries
(Reactor, RxJava, or other). (Reactor, RxJava, or other).
@ -165,7 +165,7 @@ unsure what benefits to look for, start by learning about how non-blocking I/O w
== Servers == Servers
Spring WebFlux is supported on Tomcat, Jetty, Servlet containers, as well as on Spring WebFlux is supported on Tomcat, Jetty, Servlet containers, as well as on
non-Servlet runtimes such as Netty and Undertow. All servers are adapted to a low-level, non-Servlet runtimes such as Netty. All servers are adapted to a low-level,
xref:web/webflux/reactive-spring.adoc#webflux-httphandler[common API] so that higher-level xref:web/webflux/reactive-spring.adoc#webflux-httphandler[common API] so that higher-level
xref:web/webflux/new-framework.adoc#webflux-programming-models[programming models] can be supported across servers. xref:web/webflux/new-framework.adoc#webflux-programming-models[programming models] can be supported across servers.
@ -175,7 +175,7 @@ xref:web/webflux/dispatcher-handler.adoc#webflux-framework-config[WebFlux infras
lines of code. lines of code.
Spring Boot has a WebFlux starter that automates these steps. By default, the starter uses Spring Boot has a WebFlux starter that automates these steps. By default, the starter uses
Netty, but it is easy to switch to Tomcat, Jetty, or Undertow by changing your Netty, but it is easy to switch to Tomcat, or Jetty by changing your
Maven or Gradle dependencies. Spring Boot defaults to Netty, because it is more widely Maven or Gradle dependencies. Spring Boot defaults to Netty, because it is more widely
used in the asynchronous, non-blocking space and lets a client and a server share resources. used in the asynchronous, non-blocking space and lets a client and a server share resources.
@ -188,8 +188,6 @@ adapter. It is not exposed for direct use.
NOTE: It is strongly advised not to map Servlet filters or directly manipulate the Servlet API in the context of a WebFlux application. NOTE: It is strongly advised not to map Servlet filters or directly manipulate the Servlet API in the context of a WebFlux application.
For the reasons listed above, mixing blocking I/O and non-blocking I/O in the same context will cause runtime issues. For the reasons listed above, mixing blocking I/O and non-blocking I/O in the same context will cause runtime issues.
For Undertow, Spring WebFlux uses Undertow APIs directly without the Servlet API.
[[webflux-performance]] [[webflux-performance]]
== Performance == Performance

View File

@ -0,0 +1,23 @@
[[webflux-range]]
= Range Requests
:page-section-summary-toc: 1
[.small]#xref:web/webmvc/mvc-range.adoc[See equivalent in the Servlet stack]#
Spring WebFlux supports https://datatracker.ietf.org/doc/html/rfc9110#section-14[RFC 9110]
range requests. For an overview, see the
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests[Ranger Requests]
Mozilla guide.
The `Range` header is parsed and handled transparently in WebFlux when an annotated
controller returns a `Resource` or `ResponseEntity<Resource>`, or a functional endpoint
xref:web/webflux-functional.adoc#webflux-fn-resources[serves a `Resource`]. `Range` header
support is also transparently handled when serving
xref:web/webflux/config.adoc#webflux-config-static-resources[static resources].
TIP: The `Resource` must not be an `InputStreamResource` and with `ResponseEntity<Resource>`,
the status of the response must be 200.
The underlying support is in the `HttpRange` class, which exposes methods to parse
`Range` headers and split a `Resource` into a `List<ResourceRegion>` that in turn can be
then written to the response via `ResourceRegionEncoder` and `ResourceHttpMessageWriter`.

View File

@ -7,7 +7,7 @@ applications:
* For server request processing there are two levels of support. * For server request processing there are two levels of support.
** xref:web/webflux/reactive-spring.adoc#webflux-httphandler[HttpHandler]: Basic contract for HTTP request handling with ** xref:web/webflux/reactive-spring.adoc#webflux-httphandler[HttpHandler]: Basic contract for HTTP request handling with
non-blocking I/O and Reactive Streams back pressure, along with adapters for Reactor Netty, non-blocking I/O and Reactive Streams back pressure, along with adapters for Reactor Netty,
Undertow, Tomcat, Jetty, and any Servlet container. Tomcat, Jetty, and any Servlet container.
** xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API]: Slightly higher level, general-purpose web API for ** xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[`WebHandler` API]: Slightly higher level, general-purpose web API for
request handling, on top of which concrete programming models such as annotated request handling, on top of which concrete programming models such as annotated
controllers and functional endpoints are built. controllers and functional endpoints are built.
@ -40,10 +40,6 @@ The following table describes the supported server APIs:
| Netty API | Netty API
| {reactor-github-org}/reactor-netty[Reactor Netty] | {reactor-github-org}/reactor-netty[Reactor Netty]
| Undertow
| Undertow API
| spring-web: Undertow to Reactive Streams bridge
| Tomcat | Tomcat
| Servlet non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[] | Servlet non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[]
| spring-web: Servlet non-blocking I/O to Reactive Streams bridge | spring-web: Servlet non-blocking I/O to Reactive Streams bridge
@ -67,10 +63,6 @@ The following table describes server dependencies (also see
|io.projectreactor.netty |io.projectreactor.netty
|reactor-netty |reactor-netty
|Undertow
|io.undertow
|undertow-core
|Tomcat |Tomcat
|org.apache.tomcat.embed |org.apache.tomcat.embed
|tomcat-embed-core |tomcat-embed-core
@ -104,30 +96,6 @@ Kotlin::
---- ----
====== ======
*Undertow*
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()
----
======
*Tomcat* *Tomcat*
[tabs] [tabs]
====== ======

View File

@ -46,8 +46,13 @@ directly with it.
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-resolver[See equivalent in the Reactive stack]# [.small]#xref:web/webflux-versioning.adoc#webflux-versioning-resolver[See equivalent in the Reactive stack]#
This strategy resolves the API version from a request. The MVC config provides built-in This strategy resolves the API version from a request. The MVC config provides built-in
options to resolve from a header, from a request parameter, or from the URL path. options to resolve from a header, query parameter, media type parameter,
You can also use a custom `ApiVersionResolver`. or from the URL path. You can also use a custom `ApiVersionResolver`.
NOTE: The path resolver always resolves the version from the specified path segment, or
raises `InvalidApiVersionException` otherwise, and therefore it cannot yield to other
resolvers.

View File

@ -88,37 +88,71 @@ Kotlin::
== URI patterns == URI patterns
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-uri-templates[See equivalent in the Reactive stack]# [.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-uri-templates[See equivalent in the Reactive stack]#
`@RequestMapping` methods can be mapped using URL patterns. There are two alternatives: `@RequestMapping` methods can be mapped using URL patterns.
Spring MVC is using `PathPattern` -- a pre-parsed pattern matched against the URL path also pre-parsed as `PathContainer`.
Designed for web use, this solution deals effectively with encoding and path parameters, and matches efficiently.
See xref:web/webmvc/mvc-config/path-matching.adoc[MVC config] for customizations of path matching options.
* `PathPattern` -- a pre-parsed pattern matched against the URL path also pre-parsed as NOTE: the `AntPathMatcher` variant is now deprecated because it is less efficient and the String path input is a
`PathContainer`. Designed for web use, this solution deals effectively with encoding and
path parameters, and matches efficiently.
* `AntPathMatcher` -- match String patterns against a String path. This is the original
solution also used in Spring configuration to select resources on the classpath, on the
filesystem, and other locations. It is less efficient and the String path input is a
challenge for dealing effectively with encoding and other issues with URLs. challenge for dealing effectively with encoding and other issues with URLs.
`PathPattern` is the recommended solution for web applications and it is the only choice in You can map requests by using glob patterns and wildcards:
Spring WebFlux. It was enabled for use in Spring MVC from version 5.3 and is enabled by
default from version 6.0. See xref:web/webmvc/mvc-config/path-matching.adoc[MVC config] for
customizations of path matching options.
`PathPattern` supports the same pattern syntax as `AntPathMatcher`. In addition, it also [cols="2,3,5"]
supports the capturing pattern, for example, `+{*spring}+`, for matching 0 or more path segments |===
at the end of a path. `PathPattern` also restricts the use of `+**+` for matching multiple |Pattern |Description |Example
path segments such that it's only allowed at the end of a pattern. This eliminates many
cases of ambiguity when choosing the best matching pattern for a given request.
For full pattern syntax please refer to
{spring-framework-api}/web/util/pattern/PathPattern.html[PathPattern] and
{spring-framework-api}/util/AntPathMatcher.html[AntPathMatcher].
Some example patterns: | `spring`
| Literal pattern
| `+"/spring"+` matches `+"/spring"+`
* `+"/resources/ima?e.png"+` - match one character in a path segment | `+?+`
* `+"/resources/*.png"+` - match zero or more characters in a path segment | Matches one character
* `+"/resources/**"+` - match multiple path segments | `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
* `+"/projects/{project}/versions"+` - match a path segment and capture it as a variable
* `++"/projects/{project:[a-z]+}/versions"++` - match and capture a variable with a regex | `+*+`
| Matches zero or more characters within a path segment
| `+"/resources/*.png"+` matches `+"/resources/file.png"+`
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`
| `+**+`
| Matches zero or more path segments
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
`+"/**/resources"+` matches `+"/spring/resources"+` and `+"/spring/framework/resources"+`
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
`+"/**/{name}/resources"+` is invalid as only a literal pattern is allowed right after `+**+`.
`+"/**/project/{project}/resources"+` is allowed.
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
| `+{name}+`
| Matches a path segment and captures it as a variable named "name"
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
| `+{name:[a-z]+}+`
| Matches the regexp `+"[a-z]+"+` as a path variable named "name"
| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+`
| `+{*path}+`
| Matches zero or more path segments and captures it as a variable named "path"
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
`+"/{*path}/{name}/resources"+` is invalid as only a literal pattern is allowed right after `{*path}`.
`+"/{*path}/project/{project}/resources"+` is allowed.
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|===
Captured URI variables can be accessed with `@PathVariable`. For example: Captured URI variables can be accessed with `@PathVariable`. For example:
@ -217,10 +251,13 @@ Kotlin::
---- ----
====== ======
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup URI path patterns can also have:
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this, for example, to parameterize a base URL based on - Embedded `${...}` placeholders that are resolved on startup via
some external configuration. `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. This is useful, for example, to parameterize a base URL based on
external configuration.
- SpEL expression `#{...}`.
[[mvc-ann-requestmapping-pattern-comparison]] [[mvc-ann-requestmapping-pattern-comparison]]

View File

@ -0,0 +1,23 @@
[[mvc-range]]
= Range Requests
:page-section-summary-toc: 1
[.small]#xref:web/webflux/range.adoc[See equivalent in the Reactive stack]#
Spring MVC supports https://datatracker.ietf.org/doc/html/rfc9110#section-14[RFC 9110]
range requests. For an overview, see the
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests[Ranger Requests]
Mozilla guide.
The `Range` header is parsed and handled transparently in Spring MVC when an annotated
controller returns a `Resource` or `ResponseEntity<Resource>`, or a functional endpoint
xref:web/webmvc-functional.adoc#webmvc-fn-resources[serves a `Resource`]. `Range` header
support is also transparently handled when serving
xref:web/webmvc/mvc-config/static-resources.adoc[static resources].
TIP: The `Resource` must not be an `InputStreamResource` and with `ResponseEntity<Resource>`,
the status of the response must be 200.
The underlying support is in the `HttpRange` class, which exposes methods to parse
`Range` headers and split a `Resource` into a `List<ResourceRegion>` that in turn can be
then written to the response via `ResourceRegionHttpMessageConverter`.

View File

@ -1,8 +1,8 @@
[[websocket-stomp-websocket-scope]] [[websocket-stomp-websocket-scope]]
= WebSocket Scope = WebSocket Scope
Each WebSocket session has a map of attributes. The map is attached as a header to Each WebSocket session has a map of attributes. The map is attached as a header to inbound
inbound client messages and may be accessed from a controller method, as the following example shows: client messages and may be accessed from a controller method, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
@ -20,13 +20,13 @@ public class MyController {
You can declare a Spring-managed bean in the `websocket` scope. You can declare a Spring-managed bean in the `websocket` scope.
You can inject WebSocket-scoped beans into controllers and any channel interceptors You can inject WebSocket-scoped beans into controllers and any channel interceptors
registered on the `clientInboundChannel`. Those are typically singletons and live registered on the `clientInboundChannel`. Those are typically singletons and live
longer than any individual WebSocket session. Therefore, you need to use a longer than any individual WebSocket session. Therefore, you need to use
scope proxy mode for WebSocket-scoped beans, as the following example shows: WebSocket-scoped beans in proxy mode, conveniently defined with `@WebSocketScope`:
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
@Component @Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) @WebSocketScope
public class MyBean { public class MyBean {
@PostConstruct @PostConstruct

View File

@ -0,0 +1,48 @@
/*
* Copyright 2002-2024 the original author 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.docs.integration.jms.jmssending;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.Queue;
import jakarta.jms.Session;
import org.springframework.jms.core.MessageCreator;
import org.springframework.jms.core.JmsTemplate;
public class JmsQueueSender {
private JmsTemplate jmsTemplate;
private Queue queue;
public void setConnectionFactory(ConnectionFactory cf) {
this.jmsTemplate = new JmsTemplate(cf);
}
public void setQueue(Queue queue) {
this.queue = queue;
}
public void simpleSend() {
this.jmsTemplate.send(this.queue, new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("hello queue world");
}
});
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2002-2024 the original author 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.docs.integration.jms.jmssendingconversion;
import java.util.HashMap;
import java.util.Map;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessagePostProcessor;
public class JmsSenderWithConversion {
private JmsTemplate jmsTemplate;
public void sendWithConversion() {
Map<String, Object> map = new HashMap<>();
map.put("Name", "Mark");
map.put("Age", 47);
jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() {
public Message postProcessMessage(Message message) throws JMSException {
message.setIntProperty("AccountID", 1234);
message.setJMSCorrelationID("123-00001");
return message;
}
});
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-present the original author 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.docs.integration.jms.jmssendingjmsclient;
import jakarta.jms.ConnectionFactory;
import org.springframework.jms.core.JmsClient;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
public class JmsClientSample {
private final JmsClient jmsClient;
public JmsClientSample(ConnectionFactory connectionFactory) {
// For custom options, use JmsClient.builder(ConnectionFactory)
this.jmsClient = JmsClient.create(connectionFactory);
}
public void sendWithConversion() {
this.jmsClient.destination("myQueue")
.withTimeToLive(1000)
.send("myPayload"); // optionally with a headers Map next to the payload
}
public void sendCustomMessage() {
Message<?> message = MessageBuilder.withPayload("myPayload").build(); // optionally with headers
this.jmsClient.destination("myQueue")
.withTimeToLive(1000)
.send(message);
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2002-2024 the original author 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.docs.integration.jms.jmssendingpostprocessor;
import jakarta.jms.ConnectionFactory;
import org.springframework.jms.core.JmsClient;
import org.springframework.messaging.Message;
import org.springframework.messaging.core.MessagePostProcessor;
import org.springframework.messaging.support.MessageBuilder;
public class JmsClientWithPostProcessor {
private final JmsClient jmsClient;
public JmsClientWithPostProcessor(ConnectionFactory connectionFactory) {
this.jmsClient = JmsClient.builder(connectionFactory)
.messagePostProcessor(new TenantIdMessageInterceptor("42"))
.build();
}
public void sendWithPostProcessor() {
this.jmsClient.destination("myQueue")
.withTimeToLive(1000)
.send("myPayload");
}
static class TenantIdMessageInterceptor implements MessagePostProcessor {
private final String tenantId;
public TenantIdMessageInterceptor(String tenantId) {
this.tenantId = tenantId;
}
@Override
public Message<?> postProcessMessage(Message<?> message) {
return MessageBuilder.fromMessage(message)
.setHeader("tenantId", this.tenantId)
.build();
}
}
}

View File

@ -7,37 +7,37 @@ javaPlatform {
} }
dependencies { dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4")) api(platform("com.fasterxml.jackson:jackson-bom:2.20.0-rc1"))
api(platform("io.micrometer:micrometer-bom:1.15.1")) api(platform("io.micrometer:micrometer-bom:1.16.0-M2"))
api(platform("io.netty:netty-bom:4.2.2.Final")) api(platform("io.netty:netty-bom:4.2.4.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.0-M4")) api(platform("io.projectreactor:reactor-bom:2025.0.0-M6"))
api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.27")) api(platform("org.apache.groovy:groovy-bom:5.0.0-rc-1"))
api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3")) api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3"))
api(platform("org.assertj:assertj-bom:3.27.3")) api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.0.beta1")) api(platform("org.eclipse.jetty:jetty-bom:12.1.0"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.beta1")) api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
api(platform("org.junit:junit-bom:5.13.3")) api(platform("org.junit:junit-bom:5.13.4"))
api(platform("org.mockito:mockito-bom:5.18.0")) api(platform("org.mockito:mockito-bom:5.19.0"))
api(platform("tools.jackson:jackson-bom:3.0.0-rc5")) api(platform("tools.jackson:jackson-bom:3.0.0-rc8"))
constraints { constraints {
api("com.fasterxml:aalto-xml:1.3.2") api("com.fasterxml:aalto-xml:1.3.2")
api("com.fasterxml.woodstox:woodstox-core:6.7.0") api("com.fasterxml.woodstox:woodstox-core:6.7.0")
api("com.github.ben-manes.caffeine:caffeine:3.2.1") api("com.github.ben-manes.caffeine:caffeine:3.2.2")
api("com.github.librepdf:openpdf:1.3.43") api("com.github.librepdf:openpdf:1.3.43")
api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2") api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.13.1") api("com.google.code.gson:gson:2.13.1")
api("com.google.protobuf:protobuf-java-util:4.30.2") api("com.google.protobuf:protobuf-java-util:4.32.0")
api("com.h2database:h2:2.3.232") api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.9.0") api("com.jayway.jsonpath:json-path:2.9.0")
api("com.networknt:json-schema-validator:1.5.3") api("com.networknt:json-schema-validator:1.5.3")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0") api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
api("com.rometools:rome:1.19.0") api("com.rometools:rome:1.19.0")
api("com.squareup.okhttp3:mockwebserver:3.14.9") api("com.squareup.okhttp3:mockwebserver3:5.1.0")
api("com.sun.activation:jakarta.activation:2.0.1") api("com.sun.activation:jakarta.activation:2.0.1")
api("com.sun.xml.bind:jaxb-core:3.0.2") api("com.sun.xml.bind:jaxb-core:3.0.2")
api("com.sun.xml.bind:jaxb-impl:3.0.2") api("com.sun.xml.bind:jaxb-impl:3.0.2")
@ -47,16 +47,13 @@ dependencies {
api("commons-io:commons-io:2.15.0") api("commons-io:commons-io:2.15.0")
api("commons-logging:commons-logging:1.3.5") api("commons-logging:commons-logging:1.3.5")
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
api("io.mockk:mockk:1.13.4") api("io.mockk:mockk:1.14.5")
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE") api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE") api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE") api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
api("io.reactivex.rxjava3:rxjava:3.1.10") api("io.reactivex.rxjava3:rxjava:3.1.10")
api("io.smallrye.reactive:mutiny:1.10.0") api("io.smallrye.reactive:mutiny:1.10.0")
api("io.undertow:undertow-core:2.3.18.Final")
api("io.undertow:undertow-servlet:2.3.18.Final")
api("io.undertow:undertow-websockets-jsr:2.3.18.Final")
api("io.vavr:vavr:0.10.4") api("io.vavr:vavr:0.10.4")
api("jakarta.activation:jakarta.activation-api:2.1.3") api("jakarta.activation:jakarta.activation-api:2.1.3")
api("jakarta.annotation:jakarta.annotation-api:3.0.0") api("jakarta.annotation:jakarta.annotation-api:3.0.0")
@ -124,13 +121,13 @@ dependencies {
api("org.glassfish:jakarta.el:4.0.2") api("org.glassfish:jakarta.el:4.0.2")
api("org.graalvm.sdk:graal-sdk:22.3.1") api("org.graalvm.sdk:graal-sdk:22.3.1")
api("org.hamcrest:hamcrest:3.0") api("org.hamcrest:hamcrest:3.0")
api("org.hibernate.orm:hibernate-core:7.0.5.Final") api("org.hibernate.orm:hibernate-core:7.1.0.Final")
api("org.hibernate.validator:hibernate-validator:9.0.1.Final") api("org.hibernate.validator:hibernate-validator:9.0.1.Final")
api("org.hsqldb:hsqldb:2.7.4") api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.13.0") api("org.htmlunit:htmlunit:4.15.0")
api("org.javamoney:moneta:1.4.4") api("org.javamoney:moneta:1.4.4")
api("org.jboss.logging:jboss-logging:3.6.1.Final") api("org.jboss.logging:jboss-logging:3.6.1.Final")
api("org.jruby:jruby:9.4.12.0") api("org.jruby:jruby:10.0.2.0")
api("org.jspecify:jspecify:1.0.0") api("org.jspecify:jspecify:1.0.0")
api("org.junit.support:testng-engine:1.0.5") api("org.junit.support:testng-engine:1.0.5")
api("org.mozilla:rhino:1.7.15") api("org.mozilla:rhino:1.7.15")
@ -138,14 +135,14 @@ dependencies {
api("org.python:jython-standalone:2.7.4") api("org.python:jython-standalone:2.7.4")
api("org.quartz-scheduler:quartz:2.3.2") api("org.quartz-scheduler:quartz:2.3.2")
api("org.reactivestreams:reactive-streams:1.0.4") api("org.reactivestreams:reactive-streams:1.0.4")
api("org.seleniumhq.selenium:htmlunit3-driver:4.33.0") api("org.seleniumhq.selenium:htmlunit3-driver:4.35.0")
api("org.seleniumhq.selenium:selenium-java:4.33.0") api("org.seleniumhq.selenium:selenium-java:4.35.0")
api("org.skyscreamer:jsonassert:2.0-rc1") api("org.skyscreamer:jsonassert:2.0-rc1")
api("org.testng:testng:7.11.0") api("org.testng:testng:7.11.0")
api("org.webjars:underscorejs:1.8.3") api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-lite:1.1.0") api("org.webjars:webjars-locator-lite:1.1.0")
api("org.xmlunit:xmlunit-assertj:2.10.0") api("org.xmlunit:xmlunit-assertj:2.10.3")
api("org.xmlunit:xmlunit-matchers:2.10.0") api("org.xmlunit:xmlunit-matchers:2.10.3")
api("org.yaml:snakeyaml:2.4") api("org.yaml:snakeyaml:2.4")
} }
} }

View File

@ -5,6 +5,10 @@ org.gradle.jvmargs=-Xmx2048m
org.gradle.parallel=true org.gradle.parallel=true
kotlinVersion=2.2.0 kotlinVersion=2.2.0
byteBuddyVersion=1.17.6
kotlin.jvm.target.validation.mode=ignore kotlin.jvm.target.validation.mode=ignore
kotlin.stdlib.default.dependency=false kotlin.stdlib.default.dependency=false
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled

View File

@ -1,32 +0,0 @@
tasks.findByName("dokkaHtmlPartial")?.configure {
outputDirectory.set(new File(buildDir, "docs/kdoc"))
dokkaSourceSets {
configureEach {
sourceRoots.setFrom(file("src/main/kotlin"))
classpath.from(sourceSets["main"].runtimeClasspath)
externalDocumentationLink {
url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/"))
packageListUrl.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/element-list"))
}
externalDocumentationLink {
url.set(new URL("https://projectreactor.io/docs/core/release/api/"))
}
externalDocumentationLink {
url.set(new URL("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/"))
}
externalDocumentationLink {
url.set(new URL("https://kotlin.github.io/kotlinx.coroutines/"))
}
externalDocumentationLink {
url.set(new URL("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/"))
}
externalDocumentationLink {
url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/"))
packageListUrl.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list"))
}
externalDocumentationLink {
url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/"))
}
}
}
}

View File

@ -15,11 +15,6 @@ dependencies {
jmh 'net.sf.jopt-simple:jopt-simple' jmh 'net.sf.jopt-simple:jopt-simple'
} }
pluginManager.withPlugin("kotlin") {
apply plugin: "org.jetbrains.dokka"
apply from: "${rootDir}/gradle/docs-dokka.gradle"
}
jmh { jmh {
duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE
} }

View File

@ -23,6 +23,8 @@ import org.jspecify.annotations.Nullable;
import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator; import org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator;
import org.springframework.aop.framework.ProxyConfig;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator; import org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
@ -96,17 +98,22 @@ public abstract class AopConfigUtils {
} }
public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) {
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { defaultProxyConfig(registry).getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE);
}
} }
public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) {
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { defaultProxyConfig(registry).getPropertyValues().add("exposeProxy", Boolean.TRUE);
BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); }
definition.getPropertyValues().add("exposeProxy", Boolean.TRUE);
private static BeanDefinition defaultProxyConfig(BeanDefinitionRegistry registry) {
if (registry.containsBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME)) {
return registry.getBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME);
} }
RootBeanDefinition beanDefinition = new RootBeanDefinition(ProxyConfig.class);
beanDefinition.setSource(AopConfigUtils.class);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, beanDefinition);
return beanDefinition;
} }
private static @Nullable BeanDefinition registerOrEscalateApcAsRequired( private static @Nullable BeanDefinition registerOrEscalateApcAsRequired(
@ -115,12 +122,12 @@ public abstract class AopConfigUtils {
Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); BeanDefinition beanDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
if (!cls.getName().equals(apcDefinition.getBeanClassName())) { if (!cls.getName().equals(beanDefinition.getBeanClassName())) {
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); int currentPriority = findPriorityForClass(beanDefinition.getBeanClassName());
int requiredPriority = findPriorityForClass(cls); int requiredPriority = findPriorityForClass(cls);
if (currentPriority < requiredPriority) { if (currentPriority < requiredPriority) {
apcDefinition.setBeanClassName(cls.getName()); beanDefinition.setBeanClassName(cls.getName());
} }
} }
return null; return null;
@ -128,8 +135,8 @@ public abstract class AopConfigUtils {
RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
beanDefinition.setSource(source); beanDefinition.setSource(source);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
return beanDefinition; return beanDefinition;
} }

View File

@ -112,11 +112,13 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu
if (isEligible(bean, beanName)) { if (isEligible(bean, beanName)) {
ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
if (!proxyFactory.isProxyTargetClass()) { if (!proxyFactory.isProxyTargetClass() && !proxyFactory.hasUserSuppliedInterfaces()) {
evaluateProxyInterfaces(bean.getClass(), proxyFactory); evaluateProxyInterfaces(bean.getClass(), proxyFactory);
} }
proxyFactory.addAdvisor(this.advisor); proxyFactory.addAdvisor(this.advisor);
customizeProxyFactory(proxyFactory); customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(isFrozen());
proxyFactory.setPreFiltered(true);
// Use original ClassLoader if bean class not locally loaded in overriding class loader // Use original ClassLoader if bean class not locally loaded in overriding class loader
ClassLoader classLoader = getProxyClassLoader(); ClassLoader classLoader = getProxyClassLoader();
@ -187,6 +189,7 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu
protected ProxyFactory prepareProxyFactory(Object bean, String beanName) { protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
ProxyFactory proxyFactory = new ProxyFactory(); ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this); proxyFactory.copyFrom(this);
proxyFactory.setFrozen(false);
proxyFactory.setTarget(bean); proxyFactory.setTarget(bean);
return proxyFactory; return proxyFactory;
} }

View File

@ -694,7 +694,7 @@ class CglibAopProxy implements AopProxy, Serializable {
Object target = null; Object target = null;
TargetSource targetSource = this.advised.getTargetSource(); TargetSource targetSource = this.advised.getTargetSource();
try { try {
if (this.advised.exposeProxy) { if (this.advised.isExposeProxy()) {
// Make invocation available if necessary. // Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy); oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true; setProxyContext = true;

View File

@ -183,7 +183,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
// There is only getDecoratedClass() declared -> dispatch to proxy config. // There is only getDecoratedClass() declared -> dispatch to proxy config.
return AopProxyUtils.ultimateTargetClass(this.advised); return AopProxyUtils.ultimateTargetClass(this.advised);
} }
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && else if (!this.advised.isOpaque() && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) { method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config... // Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
@ -191,7 +191,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
Object retVal; Object retVal;
if (this.advised.exposeProxy) { if (this.advised.isExposeProxy()) {
// Make invocation available if necessary. // Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy); oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true; setProxyContext = true;

View File

@ -18,6 +18,8 @@ package org.springframework.aop.framework;
import java.io.Serializable; import java.io.Serializable;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -34,15 +36,15 @@ public class ProxyConfig implements Serializable {
private static final long serialVersionUID = -8409359707199703185L; private static final long serialVersionUID = -8409359707199703185L;
private boolean proxyTargetClass = false; private @Nullable Boolean proxyTargetClass;
private boolean optimize = false; private @Nullable Boolean optimize;
boolean opaque = false; private @Nullable Boolean opaque;
boolean exposeProxy = false; private @Nullable Boolean exposeProxy;
private boolean frozen = false; private @Nullable Boolean frozen;
/** /**
@ -65,7 +67,7 @@ public class ProxyConfig implements Serializable {
* Return whether to proxy the target class directly as well as any interfaces. * Return whether to proxy the target class directly as well as any interfaces.
*/ */
public boolean isProxyTargetClass() { public boolean isProxyTargetClass() {
return this.proxyTargetClass; return (this.proxyTargetClass != null && this.proxyTargetClass);
} }
/** /**
@ -85,7 +87,7 @@ public class ProxyConfig implements Serializable {
* Return whether proxies should perform aggressive optimizations. * Return whether proxies should perform aggressive optimizations.
*/ */
public boolean isOptimize() { public boolean isOptimize() {
return this.optimize; return (this.optimize != null && this.optimize);
} }
/** /**
@ -103,7 +105,7 @@ public class ProxyConfig implements Serializable {
* prevented from being cast to {@link Advised}. * prevented from being cast to {@link Advised}.
*/ */
public boolean isOpaque() { public boolean isOpaque() {
return this.opaque; return (this.opaque != null && this.opaque);
} }
/** /**
@ -124,7 +126,7 @@ public class ProxyConfig implements Serializable {
* each invocation. * each invocation.
*/ */
public boolean isExposeProxy() { public boolean isExposeProxy() {
return this.exposeProxy; return (this.exposeProxy != null && this.exposeProxy);
} }
/** /**
@ -141,7 +143,7 @@ public class ProxyConfig implements Serializable {
* Return whether the config is frozen, and no advice changes can be made. * Return whether the config is frozen, and no advice changes can be made.
*/ */
public boolean isFrozen() { public boolean isFrozen() {
return this.frozen; return (this.frozen != null && this.frozen);
} }
@ -153,9 +155,34 @@ public class ProxyConfig implements Serializable {
Assert.notNull(other, "Other ProxyConfig object must not be null"); Assert.notNull(other, "Other ProxyConfig object must not be null");
this.proxyTargetClass = other.proxyTargetClass; this.proxyTargetClass = other.proxyTargetClass;
this.optimize = other.optimize; this.optimize = other.optimize;
this.opaque = other.opaque;
this.exposeProxy = other.exposeProxy; this.exposeProxy = other.exposeProxy;
this.frozen = other.frozen; this.frozen = other.frozen;
this.opaque = other.opaque; }
/**
* Copy default settings from the other config object,
* for settings that have not been locally set.
* @param other object to copy configuration from
* @since 7.0
*/
public void copyDefault(ProxyConfig other) {
Assert.notNull(other, "Other ProxyConfig object must not be null");
if (this.proxyTargetClass == null) {
this.proxyTargetClass = other.proxyTargetClass;
}
if (this.optimize == null) {
this.optimize = other.optimize;
}
if (this.opaque == null) {
this.opaque = other.opaque;
}
if (this.exposeProxy == null) {
this.exposeProxy = other.exposeProxy;
}
if (this.frozen == null) {
this.frozen = other.frozen;
}
} }
@Override @Override

View File

@ -117,12 +117,6 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
/** Default is global AdvisorAdapterRegistry. */ /** Default is global AdvisorAdapterRegistry. */
private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance();
/**
* Indicates whether the proxy should be frozen. Overridden from super
* to prevent the configuration from becoming frozen too early.
*/
private boolean freezeProxy = false;
/** Default is no common interceptors. */ /** Default is no common interceptors. */
private String[] interceptorNames = new String[0]; private String[] interceptorNames = new String[0];
@ -141,22 +135,6 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
private final Map<Object, Boolean> advisedBeans = new ConcurrentHashMap<>(256); private final Map<Object, Boolean> advisedBeans = new ConcurrentHashMap<>(256);
/**
* Set whether the proxy should be frozen, preventing advice
* from being added to it once it is created.
* <p>Overridden from the superclass to prevent the proxy configuration
* from being frozen before the proxy is created.
*/
@Override
public void setFrozen(boolean frozen) {
this.freezeProxy = frozen;
}
@Override
public boolean isFrozen() {
return this.freezeProxy;
}
/** /**
* Specify the {@link AdvisorAdapterRegistry} to use. * Specify the {@link AdvisorAdapterRegistry} to use.
* <p>Default is the global {@link AdvisorAdapterRegistry}. * <p>Default is the global {@link AdvisorAdapterRegistry}.
@ -206,6 +184,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
@Override @Override
public void setBeanFactory(BeanFactory beanFactory) { public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory; this.beanFactory = beanFactory;
AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory);
} }
/** /**
@ -471,6 +450,24 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
ProxyFactory proxyFactory = new ProxyFactory(); ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this); proxyFactory.copyFrom(this);
proxyFactory.setFrozen(false);
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
Class<?>[] ifcs = (this.beanFactory instanceof ConfigurableListableBeanFactory clbf ?
AutoProxyUtils.determineExposedInterfaces(clbf, beanName) : null);
if (ifcs != null) {
proxyFactory.setProxyTargetClass(false);
for (Class<?> ifc : ifcs) {
proxyFactory.addInterface(ifc);
}
}
if (ifcs != null ? ifcs.length == 0 : !proxyFactory.isProxyTargetClass()) {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
if (proxyFactory.isProxyTargetClass()) { if (proxyFactory.isProxyTargetClass()) {
// Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios) // Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
@ -481,22 +478,13 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
} }
} }
} }
else {
// No proxyTargetClass flag enforced, let's apply our default checks...
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors); proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource); proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory); customizeProxyFactory(proxyFactory);
proxyFactory.setFrozen(this.freezeProxy); proxyFactory.setFrozen(isFrozen());
if (advisorsPreFiltered()) { if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true); proxyFactory.setPreFiltered(true);
} }

View File

@ -25,9 +25,9 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
/** /**
* Extension of {@link AbstractAutoProxyCreator} which implements {@link BeanFactoryAware}, * Extension of {@link AbstractAdvisingBeanPostProcessor} which implements
* adds exposure of the original target class for each proxied bean * {@link BeanFactoryAware}, adds exposure of the original target class for each
* ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}), * proxied bean ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}),
* and participates in an externally enforced target-class mode for any given bean * and participates in an externally enforced target-class mode for any given bean
* ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}). * ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}).
* This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}. * This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}.
@ -47,6 +47,7 @@ public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends Abst
@Override @Override
public void setBeanFactory(BeanFactory beanFactory) { public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = (beanFactory instanceof ConfigurableListableBeanFactory clbf ? clbf : null); this.beanFactory = (beanFactory instanceof ConfigurableListableBeanFactory clbf ? clbf : null);
AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory);
} }
@Override @Override
@ -56,9 +57,19 @@ public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends Abst
} }
ProxyFactory proxyFactory = super.prepareProxyFactory(bean, beanName); ProxyFactory proxyFactory = super.prepareProxyFactory(bean, beanName);
if (!proxyFactory.isProxyTargetClass() && this.beanFactory != null && if (this.beanFactory != null) {
AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) { if (AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) {
proxyFactory.setProxyTargetClass(true); proxyFactory.setProxyTargetClass(true);
}
else {
Class<?>[] ifcs = AutoProxyUtils.determineExposedInterfaces(this.beanFactory, beanName);
if (ifcs != null) {
proxyFactory.setProxyTargetClass(false);
for (Class<?> ifc : ifcs) {
proxyFactory.addInterface(ifc);
}
}
}
} }
return proxyFactory; return proxyFactory;
} }

View File

@ -18,6 +18,8 @@ package org.springframework.aop.framework.autoproxy;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.aop.framework.ProxyConfig;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@ -31,9 +33,37 @@ import org.springframework.util.StringUtils;
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 2.0.3 * @since 2.0.3
* @see AbstractAutoProxyCreator * @see AbstractAutoProxyCreator
* @see AbstractBeanFactoryAwareAdvisingPostProcessor
*/ */
public abstract class AutoProxyUtils { public abstract class AutoProxyUtils {
/**
* The bean name of the internally managed auto-proxy creator.
* @since 7.0
*/
public static final String DEFAULT_PROXY_CONFIG_BEAN_NAME =
"org.springframework.aop.framework.autoproxy.defaultProxyConfig";
/**
* Bean definition attribute that may indicate the interfaces to be proxied
* (in case of it getting proxied in the first place). The value is either
* a single interface {@code Class} or an array of {@code Class}, with an
* empty array specifically signalling that all implemented interfaces need
* to be proxied.
* @since 7.0
* @see #determineExposedInterfaces
*/
public static final String EXPOSED_INTERFACES_ATTRIBUTE =
Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "exposedInterfaces");
/**
* Attribute value for specifically signalling that all implemented interfaces
* need to be proxied (through an empty {@code Class} array).
* @since 7.0
* @see #EXPOSED_INTERFACES_ATTRIBUTE
*/
public static final Object ALL_INTERFACES_ATTRIBUTE_VALUE = new Class<?>[0];
/** /**
* Bean definition attribute that may indicate whether a given bean is supposed * Bean definition attribute that may indicate whether a given bean is supposed
* to be proxied with its target class (in case of it getting proxied in the first * to be proxied with its target class (in case of it getting proxied in the first
@ -57,6 +87,47 @@ public abstract class AutoProxyUtils {
Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "originalTargetClass"); Conventions.getQualifiedAttributeName(AutoProxyUtils.class, "originalTargetClass");
/**
* Apply default ProxyConfig settings to the given ProxyConfig instance, if necessary.
* @param proxyConfig the current ProxyConfig instance
* @param beanFactory the BeanFactory to take the default ProxyConfig from
* @since 7.0
* @see #DEFAULT_PROXY_CONFIG_BEAN_NAME
* @see ProxyConfig#copyDefault
*/
static void applyDefaultProxyConfig(ProxyConfig proxyConfig, BeanFactory beanFactory) {
if (beanFactory.containsBean(DEFAULT_PROXY_CONFIG_BEAN_NAME)) {
ProxyConfig defaultProxyConfig = beanFactory.getBean(DEFAULT_PROXY_CONFIG_BEAN_NAME, ProxyConfig.class);
proxyConfig.copyDefault(defaultProxyConfig);
}
}
/**
* Determine the specific interfaces for proxying the given bean, if any.
* Checks the {@link #EXPOSED_INTERFACES_ATTRIBUTE "exposedInterfaces" attribute}
* of the corresponding bean definition.
* @param beanFactory the containing ConfigurableListableBeanFactory
* @param beanName the name of the bean
* @return whether the given bean should be proxied with its target class
* @since 7.0
* @see #EXPOSED_INTERFACES_ATTRIBUTE
*/
static Class<?> @Nullable [] determineExposedInterfaces(
ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) {
if (beanName != null && beanFactory.containsBeanDefinition(beanName)) {
BeanDefinition bd = beanFactory.getBeanDefinition(beanName);
Object interfaces = bd.getAttribute(EXPOSED_INTERFACES_ATTRIBUTE);
if (interfaces instanceof Class<?>[] ifcs) {
return ifcs;
}
else if (interfaces instanceof Class<?> ifc) {
return new Class<?>[] {ifc};
}
}
return null;
}
/** /**
* Determine whether the given bean should be proxied with its target * Determine whether the given bean should be proxied with its target
* class rather than its interfaces. Checks the * class rather than its interfaces. Checks the
@ -65,6 +136,7 @@ public abstract class AutoProxyUtils {
* @param beanFactory the containing ConfigurableListableBeanFactory * @param beanFactory the containing ConfigurableListableBeanFactory
* @param beanName the name of the bean * @param beanName the name of the bean
* @return whether the given bean should be proxied with its target class * @return whether the given bean should be proxied with its target class
* @see #PRESERVE_TARGET_CLASS_ATTRIBUTE
*/ */
public static boolean shouldProxyTargetClass( public static boolean shouldProxyTargetClass(
ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) {

View File

@ -20,8 +20,6 @@ import java.beans.BeanInfo;
import java.beans.IntrospectionException; import java.beans.IntrospectionException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.jspecify.annotations.NonNull;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
/** /**
@ -44,7 +42,7 @@ import org.springframework.core.Ordered;
public class ExtendedBeanInfoFactory extends StandardBeanInfoFactory { public class ExtendedBeanInfoFactory extends StandardBeanInfoFactory {
@Override @Override
public @NonNull BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException { public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
BeanInfo beanInfo = super.getBeanInfo(beanClass); BeanInfo beanInfo = super.getBeanInfo(beanClass);
return (supports(beanClass) ? new ExtendedBeanInfo(beanInfo) : beanInfo); return (supports(beanClass) ? new ExtendedBeanInfo(beanInfo) : beanInfo);
} }

View File

@ -514,8 +514,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
* to check whether the bean with the given name matches the specified type. Allow * to check whether the bean with the given name matches the specified type. Allow
* additional constraints to be applied to ensure that beans are not created early. * additional constraints to be applied to ensure that beans are not created early.
* @param name the name of the bean to query * @param name the name of the bean to query
* @param typeToMatch the type to match against (as a * @param typeToMatch the type to match against (as a {@code ResolvableType})
* {@code ResolvableType})
* @return {@code true} if the bean type matches, {@code false} if it * @return {@code true} if the bean type matches, {@code false} if it
* doesn't match or cannot be determined yet * doesn't match or cannot be determined yet
* @throws NoSuchBeanDefinitionException if there is no bean with the given name * @throws NoSuchBeanDefinitionException if there is no bean with the given name

View File

@ -60,6 +60,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.beans.factory.CannotLoadBeanClassException; import org.springframework.beans.factory.CannotLoadBeanClassException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
@ -191,8 +192,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
/** Map from bean name to merged BeanDefinitionHolder. */ /** Map from bean name to merged BeanDefinitionHolder. */
private final Map<String, BeanDefinitionHolder> mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256); private final Map<String, BeanDefinitionHolder> mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256);
/** Set of bean definition names with a primary marker. */ /** Map of bean definition names with a primary marker plus corresponding type. */
private final Set<String> primaryBeanNames = ConcurrentHashMap.newKeySet(16); private final Map<String, Class<?>> primaryBeanNamesWithType = new ConcurrentHashMap<>(16);
/** Map of singleton and non-singleton bean names, keyed by dependency type. */ /** Map of singleton and non-singleton bean names, keyed by dependency type. */
private final Map<Class<?>, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64); private final Map<Class<?>, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64);
@ -1024,7 +1025,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) {
super.cacheMergedBeanDefinition(mbd, beanName); super.cacheMergedBeanDefinition(mbd, beanName);
if (mbd.isPrimary()) { if (mbd.isPrimary()) {
this.primaryBeanNames.add(beanName); this.primaryBeanNamesWithType.put(beanName, Void.class);
} }
} }
@ -1309,7 +1310,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
// Cache a primary marker for the given bean. // Cache a primary marker for the given bean.
if (beanDefinition.isPrimary()) { if (beanDefinition.isPrimary()) {
this.primaryBeanNames.add(beanName); this.primaryBeanNamesWithType.put(beanName, Void.class);
} }
} }
@ -1401,7 +1402,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
destroySingleton(beanName); destroySingleton(beanName);
// Remove a cached primary marker for the given bean. // Remove a cached primary marker for the given bean.
this.primaryBeanNames.remove(beanName); this.primaryBeanNamesWithType.remove(beanName);
// Notify all post-processors that the specified bean definition has been reset. // Notify all post-processors that the specified bean definition has been reset.
for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) { for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) {
@ -1451,11 +1452,27 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
} }
} }
@Override
protected void addSingleton(String beanName, Object singletonObject) {
super.addSingleton(beanName, singletonObject);
Predicate<Class<?>> filter = (beanType -> beanType != Object.class && beanType.isInstance(singletonObject));
this.allBeanNamesByType.keySet().removeIf(filter);
this.singletonBeanNamesByType.keySet().removeIf(filter);
if (this.primaryBeanNamesWithType.containsKey(beanName) && singletonObject.getClass() != NullBean.class) {
Class<?> beanType = (singletonObject instanceof FactoryBean<?> fb ?
getTypeForFactoryBean(fb) : singletonObject.getClass());
if (beanType != null) {
this.primaryBeanNamesWithType.put(beanName, beanType);
}
}
}
@Override @Override
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException { public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
super.registerSingleton(beanName, singletonObject); super.registerSingleton(beanName, singletonObject);
updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName)); updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName));
clearByTypeCache();
} }
@Override @Override
@ -2238,8 +2255,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
* not matching the given bean name. * not matching the given bean name.
*/ */
private boolean hasPrimaryConflict(String beanName, Class<?> dependencyType) { private boolean hasPrimaryConflict(String beanName, Class<?> dependencyType) {
for (String candidate : this.primaryBeanNames) { for (Map.Entry<String, Class<?>> candidate : this.primaryBeanNamesWithType.entrySet()) {
if (isTypeMatch(candidate, dependencyType) && !candidate.equals(beanName)) { String candidateName = candidate.getKey();
Class<?> candidateType = candidate.getValue();
if (!candidateName.equals(beanName) && (candidateType != Void.class ?
dependencyType.isAssignableFrom(candidateType) : // cached singleton class for primary bean
isTypeMatch(candidateName, dependencyType))) { // not instantiated yet or not a singleton
return true; return true;
} }
} }

View File

@ -103,7 +103,7 @@ public class PathEditor extends PropertyEditorSupport {
if (resource == null) { if (resource == null) {
setValue(null); setValue(null);
} }
else if (nioPathCandidate && !resource.exists()) { else if (nioPathCandidate && (!resource.isFile() || !resource.exists())) {
setValue(Paths.get(text).normalize()); setValue(Paths.get(text).normalize());
} }
else { else {

View File

@ -3211,6 +3211,29 @@ class DefaultListableBeanFactoryTests {
assertThat(holder.getNonPublicEnum()).isEqualTo(NonPublicEnum.VALUE_1); assertThat(holder.getNonPublicEnum()).isEqualTo(NonPublicEnum.VALUE_1);
} }
@Test
void mostSpecificCacheEntryForTypeMatching() {
RootBeanDefinition bd1 = new RootBeanDefinition();
bd1.setFactoryBeanName("config");
bd1.setFactoryMethodName("create");
lbf.registerBeanDefinition("config", new RootBeanDefinition(BeanWithFactoryMethod.class));
lbf.registerBeanDefinition("bd1", bd1);
lbf.registerBeanDefinition("bd2", new RootBeanDefinition(NestedTestBean.class));
lbf.freezeConfiguration();
String[] allBeanNames = lbf.getBeanNamesForType(Object.class);
String[] nestedBeanNames = lbf.getBeanNamesForType(NestedTestBean.class);
assertThat(lbf.getType("bd1")).isEqualTo(TestBean.class);
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).isEmpty();
lbf.getBean("bd1");
assertThat(lbf.getType("bd1")).isEqualTo(DerivedTestBean.class);
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).containsExactly("bd1");
assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames);
assertThat(lbf.getBeanNamesForType(Object.class)).isSameAs(allBeanNames);
}
private int registerBeanDefinitions(Properties p) { private int registerBeanDefinitions(Properties p) {
return registerBeanDefinitions(p, null); return registerBeanDefinitions(p, null);
@ -3427,7 +3450,7 @@ class DefaultListableBeanFactoryTests {
} }
public TestBean create() { public TestBean create() {
TestBean tb = new TestBean(); DerivedTestBean tb = new DerivedTestBean();
tb.setName(this.name); tb.setName(this.name);
return tb; return tb;
} }
@ -3655,11 +3678,11 @@ class DefaultListableBeanFactoryTests {
private FactoryBean<?> factoryBean; private FactoryBean<?> factoryBean;
public final FactoryBean<?> getFactoryBean() { public FactoryBean<?> getFactoryBean() {
return this.factoryBean; return this.factoryBean;
} }
public final void setFactoryBean(final FactoryBean<?> factoryBean) { public void setFactoryBean(FactoryBean<?> factoryBean) {
this.factoryBean = factoryBean; this.factoryBean = factoryBean;
} }
} }

View File

@ -35,6 +35,7 @@ import org.quartz.utils.DBConnectionManager;
import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.MetaDataAccessException; import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.util.Assert;
/** /**
* Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed * Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed
@ -88,6 +89,8 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
private @Nullable DataSource dataSource; private @Nullable DataSource dataSource;
private @Nullable DataSource nonTransactionalDataSource;
@Override @Override
@SuppressWarnings("NullAway") // Dataflow analysis limitation @SuppressWarnings("NullAway") // Dataflow analysis limitation
@ -98,11 +101,40 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
throw new SchedulerConfigException("No local DataSource found for configuration - " + throw new SchedulerConfigException("No local DataSource found for configuration - " +
"'dataSource' property must be set on SchedulerFactoryBean"); "'dataSource' property must be set on SchedulerFactoryBean");
} }
// Non-transactional DataSource is optional: fall back to default
// DataSource if not explicitly specified.
this.nonTransactionalDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
// Configure transactional connection settings for Quartz. // Configure connection settings for Quartz.
setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName()); setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName());
setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
setDontSetAutoCommitFalse(true); setDontSetAutoCommitFalse(true);
initializeConnectionProvider();
// No, if HSQL is the platform, we really don't want to use locks...
try {
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
DatabaseMetaData::getDatabaseProductName);
productName = JdbcUtils.commonDatabaseName(productName);
if (productName != null && productName.toLowerCase(Locale.ROOT).contains("hsql")) {
setUseDBLocks(false);
setLockHandler(new SimpleSemaphore());
}
}
catch (MetaDataAccessException ex) {
logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
}
super.initialize(loadHelper, signaler);
}
void initializeConnectionProvider() {
final DataSource dataSourceToUse = this.dataSource;
Assert.state(dataSourceToUse != null, "DataSource must not be null");
final DataSource nonTxDataSourceToUse =
(this.nonTransactionalDataSource != null ? this.nonTransactionalDataSource : dataSourceToUse);
// Register transactional ConnectionProvider for Quartz. // Register transactional ConnectionProvider for Quartz.
DBConnectionManager.getInstance().addConnectionProvider( DBConnectionManager.getInstance().addConnectionProvider(
TX_DATA_SOURCE_PREFIX + getInstanceName(), TX_DATA_SOURCE_PREFIX + getInstanceName(),
@ -110,7 +142,7 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
@Override @Override
public Connection getConnection() throws SQLException { public Connection getConnection() throws SQLException {
// Return a transactional Connection, if any. // Return a transactional Connection, if any.
return DataSourceUtils.doGetConnection(dataSource); return DataSourceUtils.doGetConnection(dataSourceToUse);
} }
@Override @Override
public void shutdown() { public void shutdown() {
@ -123,14 +155,6 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
} }
); );
// Non-transactional DataSource is optional: fall back to default
// DataSource if not explicitly specified.
DataSource nonTxDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
final DataSource nonTxDataSourceToUse = (nonTxDataSource != null ? nonTxDataSource : this.dataSource);
// Configure non-transactional connection settings for Quartz.
setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
// Register non-transactional ConnectionProvider for Quartz. // Register non-transactional ConnectionProvider for Quartz.
DBConnectionManager.getInstance().addConnectionProvider( DBConnectionManager.getInstance().addConnectionProvider(
NON_TX_DATA_SOURCE_PREFIX + getInstanceName(), NON_TX_DATA_SOURCE_PREFIX + getInstanceName(),
@ -150,23 +174,6 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
} }
} }
); );
// No, if HSQL is the platform, we really don't want to use locks...
try {
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
DatabaseMetaData::getDatabaseProductName);
productName = JdbcUtils.commonDatabaseName(productName);
if (productName != null && productName.toLowerCase(Locale.ROOT).contains("hsql")) {
setUseDBLocks(false);
setLockHandler(new SimpleSemaphore());
}
}
catch (MetaDataAccessException ex) {
logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
}
super.initialize(loadHelper, signaler);
} }
@Override @Override

View File

@ -28,6 +28,8 @@ import org.jspecify.annotations.Nullable;
import org.quartz.Scheduler; import org.quartz.Scheduler;
import org.quartz.SchedulerException; import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory; import org.quartz.SchedulerFactory;
import org.quartz.core.QuartzScheduler;
import org.quartz.core.QuartzSchedulerResources;
import org.quartz.impl.RemoteScheduler; import org.quartz.impl.RemoteScheduler;
import org.quartz.impl.SchedulerRepository; import org.quartz.impl.SchedulerRepository;
import org.quartz.impl.StdSchedulerFactory; import org.quartz.impl.StdSchedulerFactory;
@ -165,7 +167,7 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
private @Nullable SchedulerFactory schedulerFactory; private @Nullable SchedulerFactory schedulerFactory;
private Class<? extends SchedulerFactory> schedulerFactoryClass = StdSchedulerFactory.class; private Class<? extends SchedulerFactory> schedulerFactoryClass = LocalSchedulerFactory.class;
private @Nullable String schedulerName; private @Nullable String schedulerName;
@ -203,6 +205,8 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
private @Nullable Scheduler scheduler; private @Nullable Scheduler scheduler;
private @Nullable LocalDataSourceJobStore jobStore;
/** /**
* Set an external Quartz {@link SchedulerFactory} instance to use. * Set an external Quartz {@link SchedulerFactory} instance to use.
@ -223,11 +227,12 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
/** /**
* Set the Quartz {@link SchedulerFactory} implementation to use. * Set the Quartz {@link SchedulerFactory} implementation to use.
* <p>Default is the {@link StdSchedulerFactory} class, reading in the standard * <p>Default is a Spring-internal subclass of the {@link StdSchedulerFactory}
* {@code quartz.properties} from {@code quartz.jar}. For applying custom Quartz * class, reading in the standard {@code quartz.properties} from
* properties, specify {@link #setConfigLocation "configLocation"} and/or * {@code quartz.jar}. For applying custom Quartz properties,
* {@link #setQuartzProperties "quartzProperties"} etc on this local * specify {@link #setConfigLocation "configLocation"} and/or
* {@code SchedulerFactoryBean} instance. * {@link #setQuartzProperties "quartzProperties"} etc on this
* local {@code SchedulerFactoryBean} instance.
* @see org.quartz.impl.StdSchedulerFactory * @see org.quartz.impl.StdSchedulerFactory
* @see #setConfigLocation * @see #setConfigLocation
* @see #setQuartzProperties * @see #setQuartzProperties
@ -508,8 +513,9 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
private SchedulerFactory prepareSchedulerFactory() throws SchedulerException, IOException { private SchedulerFactory prepareSchedulerFactory() throws SchedulerException, IOException {
SchedulerFactory schedulerFactory = this.schedulerFactory; SchedulerFactory schedulerFactory = this.schedulerFactory;
if (schedulerFactory == null) { if (schedulerFactory == null) {
// Create local SchedulerFactory instance (typically a StdSchedulerFactory) // Create local SchedulerFactory instance (typically a LocalSchedulerFactory)
schedulerFactory = BeanUtils.instantiateClass(this.schedulerFactoryClass); schedulerFactory = (this.schedulerFactoryClass == LocalSchedulerFactory.class ?
new LocalSchedulerFactory() : BeanUtils.instantiateClass(this.schedulerFactoryClass));
if (schedulerFactory instanceof StdSchedulerFactory stdSchedulerFactory) { if (schedulerFactory instanceof StdSchedulerFactory stdSchedulerFactory) {
initSchedulerFactory(stdSchedulerFactory); initSchedulerFactory(stdSchedulerFactory);
} }
@ -778,6 +784,9 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
@Override @Override
public void start() throws SchedulingException { public void start() throws SchedulingException {
if (this.scheduler != null) { if (this.scheduler != null) {
if (this.jobStore != null) {
this.jobStore.initializeConnectionProvider();
}
try { try {
startScheduler(this.scheduler, this.startupDelay); startScheduler(this.scheduler, this.startupDelay);
} }
@ -829,4 +838,16 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
} }
} }
private class LocalSchedulerFactory extends StdSchedulerFactory {
@Override
protected Scheduler instantiate(QuartzSchedulerResources rsrcs, QuartzScheduler qs) {
if (rsrcs.getJobStore() instanceof LocalDataSourceJobStore ldsjs) {
SchedulerFactoryBean.this.jobStore = ldsjs;
}
return super.instantiate(rsrcs, qs);
}
}
} }

View File

@ -391,6 +391,7 @@ class QuartzSupportTests {
try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) { try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class)); JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class));
assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse(); assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse();
ctx.restart();
} }
} }

View File

@ -5,28 +5,28 @@
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers" ref="trigger" /> <property name="triggers" ref="trigger"/>
<property name="dataSource" ref="dataSource" /> <property name="dataSource" ref="dataSource"/>
</bean> </bean>
<bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean"> <bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="repeatInterval" value="1000" /> <property name="repeatInterval" value="1000"/>
<property name="repeatCount" value="1" /> <property name="repeatCount" value="1"/>
<property name="jobDetail"> <property name="jobDetail">
<bean class="org.springframework.scheduling.quartz.JobDetailFactoryBean"> <bean class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobDataAsMap"> <property name="jobDataAsMap">
<map> <map>
<entry key="param" value="10" /> <entry key="param" value="10"/>
</map> </map>
</property> </property>
<property name="jobClass" value="org.springframework.scheduling.quartz.QuartzSupportTests$DummyJob" /> <property name="jobClass" value="org.springframework.scheduling.quartz.QuartzSupportTests$DummyJob"/>
<property name="durability" value="true" /> <property name="durability" value="true"/>
</bean> </bean>
</property> </property>
</bean> </bean>
<jdbc:embedded-database id="dataSource" type="HSQL"> <jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script location="org/springframework/scheduling/quartz/quartz-hsql.sql" /> <jdbc:script location="org/springframework/scheduling/quartz/quartz-hsql.sql"/>
</jdbc:embedded-database> </jdbc:embedded-database>
</beans> </beans>

View File

@ -48,7 +48,7 @@ class CacheEvaluationContext extends MethodBasedEvaluationContext {
private final Set<String> unavailableVariables = new HashSet<>(1); private final Set<String> unavailableVariables = new HashSet<>(1);
CacheEvaluationContext(Object rootObject, Method method, @Nullable Object[] arguments, CacheEvaluationContext(@Nullable Object rootObject, Method method, @Nullable Object[] arguments,
ParameterNameDiscoverer parameterNameDiscoverer) { ParameterNameDiscoverer parameterNameDiscoverer) {
super(rootObject, method, arguments, parameterNameDiscoverer); super(rootObject, method, arguments, parameterNameDiscoverer);

View File

@ -221,16 +221,27 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life
void refresh() throws BeansException, IllegalStateException; void refresh() throws BeansException, IllegalStateException;
/** /**
* Stop all beans in this application context if necessary, and subsequently * Pause all beans in this application context if necessary, and subsequently
* restart all auto-startup beans, effectively restoring the lifecycle state * restart all auto-startup beans, effectively restoring the lifecycle state
* after {@link #refresh()} (typically after a preceding {@link #stop()} call * after {@link #refresh()} (typically after a preceding {@link #pause()} call
* when a full {@link #start()} of even lazy-starting beans is to be avoided). * when a full {@link #start()} of even lazy-starting beans is to be avoided).
* @since 7.0 * @since 7.0
* @see #stop() * @see #pause()
* @see #start()
* @see SmartLifecycle#isAutoStartup() * @see SmartLifecycle#isAutoStartup()
*/ */
void restart(); void restart();
/**
* Stop all beans in this application context unless they explicitly opt out of
* pausing through {@link SmartLifecycle#isPauseable()} returning {@code false}.
* @since 7.0
* @see #restart()
* @see #stop()
* @see SmartLifecycle#isPauseable()
*/
void pause();
/** /**
* Register a shutdown hook with the JVM runtime, closing this context * Register a shutdown hook with the JVM runtime, closing this context
* on JVM shutdown unless it has already been closed at that time. * on JVM shutdown unless it has already been closed at that time.

View File

@ -44,6 +44,15 @@ public interface LifecycleProcessor extends Lifecycle {
start(); start();
} }
/**
* Notification of context pause for auto-stopping components.
* @since 7.0
* @see ConfigurableApplicationContext#pause()
*/
default void onPause() {
stop();
}
/** /**
* Notification of context close phase for auto-stopping components * Notification of context close phase for auto-stopping components
* before destruction. * before destruction.

View File

@ -54,7 +54,7 @@ public interface MessageSource {
* @see #getMessage(MessageSourceResolvable, Locale) * @see #getMessage(MessageSourceResolvable, Locale)
* @see java.text.MessageFormat * @see java.text.MessageFormat
*/ */
@Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale); @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, @Nullable Locale locale);
/** /**
* Try to resolve the message. Treat as an error if the message can't be found. * Try to resolve the message. Treat as an error if the message can't be found.
@ -70,7 +70,7 @@ public interface MessageSource {
* @see #getMessage(MessageSourceResolvable, Locale) * @see #getMessage(MessageSourceResolvable, Locale)
* @see java.text.MessageFormat * @see java.text.MessageFormat
*/ */
String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException; String getMessage(String code, Object @Nullable [] args, @Nullable Locale locale) throws NoSuchMessageException;
/** /**
* Try to resolve the message using all the attributes contained within the * Try to resolve the message using all the attributes contained within the
@ -90,6 +90,6 @@ public interface MessageSource {
* @see MessageSourceResolvable#getDefaultMessage() * @see MessageSourceResolvable#getDefaultMessage()
* @see java.text.MessageFormat * @see java.text.MessageFormat
*/ */
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException; String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException;
} }

View File

@ -85,7 +85,7 @@ public interface SmartLifecycle extends Lifecycle, Phased {
/** /**
* Returns {@code true} if this {@code Lifecycle} component should get * Returns {@code true} if this {@code Lifecycle} component should get
* started automatically by the container at the time that the containing * started automatically by the container at the time that the containing
* {@link ApplicationContext} gets refreshed. * {@link ApplicationContext} gets refreshed or restarted.
* <p>A value of {@code false} indicates that the component is intended to * <p>A value of {@code false} indicates that the component is intended to
* be started through an explicit {@link #start()} call instead, analogous * be started through an explicit {@link #start()} call instead, analogous
* to a plain {@link Lifecycle} implementation. * to a plain {@link Lifecycle} implementation.
@ -93,12 +93,35 @@ public interface SmartLifecycle extends Lifecycle, Phased {
* @see #start() * @see #start()
* @see #getPhase() * @see #getPhase()
* @see LifecycleProcessor#onRefresh() * @see LifecycleProcessor#onRefresh()
* @see LifecycleProcessor#onRestart()
* @see ConfigurableApplicationContext#refresh() * @see ConfigurableApplicationContext#refresh()
* @see ConfigurableApplicationContext#restart()
*/ */
default boolean isAutoStartup() { default boolean isAutoStartup() {
return true; return true;
} }
/**
* Returns {@code true} if this {@code Lifecycle} component is able to
* participate in a restart sequence, receiving corresponding {@link #stop()}
* and {@link #start()} calls with a potential pause in-between.
* <p>A value of {@code false} indicates that the component prefers to
* be skipped in a pause scenario, neither receiving a {@link #stop()}
* call nor a subsequent {@link #start()} call, analogous to a plain
* {@link Lifecycle} implementation. It will only receive a {@link #stop()}
* call on close and on explicit context-wide stopping but not on pause.
* <p>The default implementation returns {@code true}.
* @since 7.0
* @see #stop()
* @see LifecycleProcessor#onPause()
* @see LifecycleProcessor#onClose()
* @see ConfigurableApplicationContext#pause()
* @see ConfigurableApplicationContext#close()
*/
default boolean isPauseable() {
return true;
}
/** /**
* Indicates that a Lifecycle component must stop if it is currently running. * Indicates that a Lifecycle component must stop if it is currently running.
* <p>The provided callback is used by the {@link LifecycleProcessor} to support * <p>The provided callback is used by the {@link LifecycleProcessor} to support

View File

@ -22,6 +22,7 @@ import java.util.function.Predicate;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
@ -258,6 +259,20 @@ public abstract class AnnotationConfigUtils {
if (description != null) { if (description != null) {
abd.setDescription(description.getString("value")); abd.setDescription(description.getString("value"));
} }
AnnotationAttributes proxyable = attributesFor(metadata, Proxyable.class);
if (proxyable != null) {
ProxyType mode = proxyable.getEnum("value");
if (mode == ProxyType.TARGET_CLASS) {
abd.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
}
else {
Class<?>[] ifcs = proxyable.getClassArray("interfaces");
if (ifcs.length > 0 || mode == ProxyType.INTERFACES) {
abd.setAttribute(AutoProxyUtils.EXPOSED_INTERFACES_ATTRIBUTE, ifcs);
}
}
}
} }
static BeanDefinitionHolder applyScopedProxyMode( static BeanDefinitionHolder applyScopedProxyMode(

View File

@ -0,0 +1,45 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.annotation;
/**
* Common enum for indicating a desired proxy type.
*
* @author Juergen Hoeller
* @since 7.0
* @see Proxyable#value()
*/
public enum ProxyType {
/**
* Default is a JDK dynamic proxy, or potentially a class-based CGLIB proxy
* when globally configured.
*/
DEFAULT,
/**
* Suggest a JDK dynamic proxy implementing <i>all</i> interfaces exposed by
* the class of the target object. Overrides a globally configured default.
*/
INTERFACES,
/**
* Suggest a class-based CGLIB proxy. Overrides a globally configured default.
*/
TARGET_CLASS
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Common annotation for suggesting a specific proxy type for a {@link Bean @Bean}
* method or {@link org.springframework.stereotype.Component @Component} class,
* overriding a globally configured default.
*
* <p>Only actually applying in case of a bean actually getting auto-proxied in
* the first place. Actual auto-proxying is dependent on external configuration.
*
* @author Juergen Hoeller
* @since 7.0
* @see org.springframework.aop.framework.autoproxy.AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE
* @see org.springframework.aop.framework.autoproxy.AutoProxyUtils#EXPOSED_INTERFACES_ATTRIBUTE
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Proxyable {
/**
* Suggest a specific proxy type, either {@link ProxyType#INTERFACES} for
* a JDK dynamic proxy or {@link ProxyType#TARGET_CLASS} for a CGLIB proxy,
* overriding a globally configured default.
*/
ProxyType value() default ProxyType.DEFAULT;
/**
* Suggest a JDK dynamic proxy with specific interfaces to expose, overriding
* a globally configured default.
* <p>Only taken into account if {@link #value()} is not {@link ProxyType#TARGET_CLASS}.
*/
Class<?>[] interfaces() default {};
}

View File

@ -51,9 +51,9 @@ import org.springframework.core.annotation.AliasFor;
* ignored. * ignored.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @since 6.2
* @see Reflective @Reflective * @see Reflective @Reflective
* @see RegisterReflection @RegisterReflection * @see RegisterReflection @RegisterReflection
* @since 6.2
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.event;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Event raised when an {@code ApplicationContext} gets paused.
*
* <p>Note that {@code ContextPausedEvent} is a specialization of
* {@link ContextStoppedEvent}.
*
* @author Juergen Hoeller
* @since 7.0
* @see ConfigurableApplicationContext#pause()
* @see ContextRestartedEvent
* @see ContextStoppedEvent
*/
@SuppressWarnings("serial")
public class ContextPausedEvent extends ContextStoppedEvent {
/**
* Create a new {@code ContextPausedEvent}.
* @param source the {@code ApplicationContext} that has been paused
* (must not be {@code null})
*/
public ContextPausedEvent(ApplicationContext source) {
super(source);
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.context.event; package org.springframework.context.event;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
/** /**
* Event raised when an {@code ApplicationContext} gets restarted. * Event raised when an {@code ApplicationContext} gets restarted.
@ -26,8 +27,9 @@ import org.springframework.context.ApplicationContext;
* *
* @author Sam Brannen * @author Sam Brannen
* @since 7.0 * @since 7.0
* @see ConfigurableApplicationContext#restart()
* @see ContextPausedEvent
* @see ContextStartedEvent * @see ContextStartedEvent
* @see ContextStoppedEvent
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class ContextRestartedEvent extends ContextStartedEvent { public class ContextRestartedEvent extends ContextStartedEvent {

View File

@ -53,7 +53,7 @@ public class MethodBasedEvaluationContext extends StandardEvaluationContext {
private boolean argumentsLoaded = false; private boolean argumentsLoaded = false;
public MethodBasedEvaluationContext(Object rootObject, Method method, @Nullable Object[] arguments, public MethodBasedEvaluationContext(@Nullable Object rootObject, Method method, @Nullable Object[] arguments,
ParameterNameDiscoverer parameterNameDiscoverer) { ParameterNameDiscoverer parameterNameDiscoverer) {
super(rootObject); super(rootObject);

View File

@ -66,6 +66,7 @@ import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.ResourceLoaderAware; import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.event.ApplicationEventMulticaster; import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextPausedEvent;
import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextRestartedEvent; import org.springframework.context.event.ContextRestartedEvent;
import org.springframework.context.event.ContextStartedEvent; import org.springframework.context.event.ContextStartedEvent;
@ -1486,17 +1487,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
//--------------------------------------------------------------------- //---------------------------------------------------------------------
@Override @Override
public @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale) { public @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, @Nullable Locale locale) {
return getMessageSource().getMessage(code, args, defaultMessage, locale); return getMessageSource().getMessage(code, args, defaultMessage, locale);
} }
@Override @Override
public String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException { public String getMessage(String code, Object @Nullable [] args, @Nullable Locale locale) throws NoSuchMessageException {
return getMessageSource().getMessage(code, args, locale); return getMessageSource().getMessage(code, args, locale);
} }
@Override @Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
return getMessageSource().getMessage(resolvable, locale); return getMessageSource().getMessage(resolvable, locale);
} }
@ -1555,6 +1556,12 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
publishEvent(new ContextRestartedEvent(this)); publishEvent(new ContextRestartedEvent(this));
} }
@Override
public void pause() {
getLifecycleProcessor().onPause();
publishEvent(new ContextPausedEvent(this));
}
@Override @Override
public boolean isRunning() { public boolean isRunning() {
return (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()); return (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning());

View File

@ -134,7 +134,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
@Override @Override
public final @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale) { public final @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, @Nullable Locale locale) {
String msg = getMessageInternal(code, args, locale); String msg = getMessageInternal(code, args, locale);
if (msg != null) { if (msg != null) {
return msg; return msg;
@ -146,7 +146,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
} }
@Override @Override
public final String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException { public final String getMessage(String code, Object @Nullable [] args, @Nullable Locale locale) throws NoSuchMessageException {
String msg = getMessageInternal(code, args, locale); String msg = getMessageInternal(code, args, locale);
if (msg != null) { if (msg != null) {
return msg; return msg;
@ -155,11 +155,16 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
if (fallback != null) { if (fallback != null) {
return fallback; return fallback;
} }
throw new NoSuchMessageException(code, locale); if (locale == null ) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
} }
@Override @Override
public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { public final String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
String[] codes = resolvable.getCodes(); String[] codes = resolvable.getCodes();
if (codes != null) { if (codes != null) {
for (String code : codes) { for (String code : codes) {
@ -173,7 +178,13 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
if (defaultMessage != null) { if (defaultMessage != null) {
return defaultMessage; return defaultMessage;
} }
throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "", locale); String code = !ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : "";
if (locale == null ) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
} }
@ -277,7 +288,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
* @see #renderDefaultMessage(String, Object[], Locale) * @see #renderDefaultMessage(String, Object[], Locale)
* @see #getDefaultMessage(String) * @see #getDefaultMessage(String)
*/ */
protected @Nullable String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) { protected @Nullable String getDefaultMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) {
String defaultMessage = resolvable.getDefaultMessage(); String defaultMessage = resolvable.getDefaultMessage();
String[] codes = resolvable.getCodes(); String[] codes = resolvable.getCodes();
if (defaultMessage != null) { if (defaultMessage != null) {
@ -323,7 +334,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
* @return an array of arguments with any MessageSourceResolvables resolved * @return an array of arguments with any MessageSourceResolvables resolved
*/ */
@Override @Override
protected Object[] resolveArguments(Object @Nullable [] args, Locale locale) { protected Object[] resolveArguments(Object @Nullable [] args, @Nullable Locale locale) {
if (ObjectUtils.isEmpty(args)) { if (ObjectUtils.isEmpty(args)) {
return super.resolveArguments(args, locale); return super.resolveArguments(args, locale);
} }

View File

@ -287,7 +287,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
*/ */
@Override @Override
public void stop() { public void stop() {
stopBeans(); stopBeans(false);
this.running = false; this.running = false;
} }
@ -308,7 +308,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
catch (ApplicationContextException ex) { catch (ApplicationContextException ex) {
// Some bean failed to auto-start within context refresh: // Some bean failed to auto-start within context refresh:
// stop already started beans on context refresh failure. // stop already started beans on context refresh failure.
stopBeans(); stopBeans(false);
throw ex; throw ex;
} }
this.running = true; this.running = true;
@ -318,15 +318,23 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
public void onRestart() { public void onRestart() {
this.stoppedBeans = null; this.stoppedBeans = null;
if (this.running) { if (this.running) {
stopBeans(); stopBeans(true);
} }
startBeans(true); startBeans(true);
this.running = true; this.running = true;
} }
@Override
public void onPause() {
if (this.running) {
stopBeans(true);
this.running = false;
}
}
@Override @Override
public void onClose() { public void onClose() {
stopBeans(); stopBeans(false);
this.running = false; this.running = false;
} }
@ -341,7 +349,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
void stopForRestart() { void stopForRestart() {
if (this.running) { if (this.running) {
this.stoppedBeans = ConcurrentHashMap.newKeySet(); this.stoppedBeans = ConcurrentHashMap.newKeySet();
stopBeans(); stopBeans(false);
this.running = false; this.running = false;
} }
} }
@ -361,7 +369,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
lifecycleBeans.forEach((beanName, bean) -> { lifecycleBeans.forEach((beanName, bean) -> {
if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) { if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
int startupPhase = getPhase(bean); int startupPhase = getPhase(bean);
phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly)) phases.computeIfAbsent(
startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly, false))
.add(beanName, bean); .add(beanName, bean);
} }
}); });
@ -424,13 +433,14 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
(!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())); (!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup()));
} }
private void stopBeans() { private void stopBeans(boolean pauseableOnly) {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans(); Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder()); Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
lifecycleBeans.forEach((beanName, bean) -> { lifecycleBeans.forEach((beanName, bean) -> {
int shutdownPhase = getPhase(bean); int shutdownPhase = getPhase(bean);
phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false)) phases.computeIfAbsent(
shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false, pauseableOnly))
.add(beanName, bean); .add(beanName, bean);
}); });
@ -446,13 +456,13 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
* @param beanName the name of the bean to stop * @param beanName the name of the bean to stop
*/ */
private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName, private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName,
final CountDownLatch latch, final Set<String> countDownBeanNames) { boolean pauseableOnly, final CountDownLatch latch, final Set<String> countDownBeanNames) {
Lifecycle bean = lifecycleBeans.remove(beanName); Lifecycle bean = lifecycleBeans.remove(beanName);
if (bean != null) { if (bean != null) {
String[] dependentBeans = getBeanFactory().getDependentBeans(beanName); String[] dependentBeans = getBeanFactory().getDependentBeans(beanName);
for (String dependentBean : dependentBeans) { for (String dependentBean : dependentBeans) {
doStop(lifecycleBeans, dependentBean, latch, countDownBeanNames); doStop(lifecycleBeans, dependentBean, pauseableOnly, latch, countDownBeanNames);
} }
try { try {
if (bean.isRunning()) { if (bean.isRunning()) {
@ -461,20 +471,26 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
stoppedBeans.add(beanName); stoppedBeans.add(beanName);
} }
if (bean instanceof SmartLifecycle smartLifecycle) { if (bean instanceof SmartLifecycle smartLifecycle) {
if (logger.isTraceEnabled()) { if (!pauseableOnly || smartLifecycle.isPauseable()) {
logger.trace("Asking bean '" + beanName + "' of type [" + if (logger.isTraceEnabled()) {
bean.getClass().getName() + "] to stop"); logger.trace("Asking bean '" + beanName + "' of type [" +
} bean.getClass().getName() + "] to stop");
countDownBeanNames.add(beanName);
smartLifecycle.stop(() -> {
latch.countDown();
countDownBeanNames.remove(beanName);
if (logger.isDebugEnabled()) {
logger.debug("Bean '" + beanName + "' completed its stop procedure");
} }
}); countDownBeanNames.add(beanName);
smartLifecycle.stop(() -> {
latch.countDown();
countDownBeanNames.remove(beanName);
if (logger.isDebugEnabled()) {
logger.debug("Bean '" + beanName + "' completed its stop procedure");
}
});
}
else {
// Don't wait for beans that aren't pauseable...
latch.countDown();
}
} }
else { else if (!pauseableOnly) {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Stopping bean '" + beanName + "' of type [" + logger.trace("Stopping bean '" + beanName + "' of type [" +
bean.getClass().getName() + "]"); bean.getClass().getName() + "]");
@ -562,14 +578,19 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
private final boolean autoStartupOnly; private final boolean autoStartupOnly;
private final boolean pauseableOnly;
private final List<LifecycleGroupMember> members = new ArrayList<>(); private final List<LifecycleGroupMember> members = new ArrayList<>();
private int smartMemberCount; private int smartMemberCount;
public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans, boolean autoStartupOnly) { public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans,
boolean autoStartupOnly, boolean pauseableOnly) {
this.phase = phase; this.phase = phase;
this.lifecycleBeans = lifecycleBeans; this.lifecycleBeans = lifecycleBeans;
this.autoStartupOnly = autoStartupOnly; this.autoStartupOnly = autoStartupOnly;
this.pauseableOnly = pauseableOnly;
} }
public void add(String name, Lifecycle bean) { public void add(String name, Lifecycle bean) {
@ -621,7 +642,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
Set<String> lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet()); Set<String> lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet());
for (LifecycleGroupMember member : this.members) { for (LifecycleGroupMember member : this.members) {
if (lifecycleBeanNames.contains(member.name)) { if (lifecycleBeanNames.contains(member.name)) {
doStop(this.lifecycleBeans, member.name, latch, countDownBeanNames); doStop(this.lifecycleBeans, member.name, this.pauseableOnly, latch, countDownBeanNames);
} }
else if (member.bean instanceof SmartLifecycle) { else if (member.bean instanceof SmartLifecycle) {
// Already removed: must have been a dependent bean from another phase // Already removed: must have been a dependent bean from another phase

View File

@ -53,7 +53,7 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
@Override @Override
public @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale) { public @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, @Nullable Locale locale) {
if (this.parentMessageSource != null) { if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, defaultMessage, locale); return this.parentMessageSource.getMessage(code, args, defaultMessage, locale);
} }
@ -66,17 +66,22 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
} }
@Override @Override
public String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException { public String getMessage(String code, Object @Nullable [] args, @Nullable Locale locale) throws NoSuchMessageException {
if (this.parentMessageSource != null) { if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(code, args, locale); return this.parentMessageSource.getMessage(code, args, locale);
} }
else { else {
throw new NoSuchMessageException(code, locale); if (locale == null) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
} }
} }
@Override @Override
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
if (this.parentMessageSource != null) { if (this.parentMessageSource != null) {
return this.parentMessageSource.getMessage(resolvable, locale); return this.parentMessageSource.getMessage(resolvable, locale);
} }
@ -86,7 +91,12 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
} }
String[] codes = resolvable.getCodes(); String[] codes = resolvable.getCodes();
String code = (codes != null && codes.length > 0 ? codes[0] : ""); String code = (codes != null && codes.length > 0 ? codes[0] : "");
throw new NoSuchMessageException(code, locale); if (locale == null) {
throw new NoSuchMessageException(code);
}
else {
throw new NoSuchMessageException(code, locale);
}
} }
} }

View File

@ -98,7 +98,7 @@ public abstract class MessageSourceSupport {
* @return the rendered default message (with resolved arguments) * @return the rendered default message (with resolved arguments)
* @see #formatMessage(String, Object[], java.util.Locale) * @see #formatMessage(String, Object[], java.util.Locale)
*/ */
protected String renderDefaultMessage(String defaultMessage, Object @Nullable [] args, Locale locale) { protected String renderDefaultMessage(String defaultMessage, Object @Nullable [] args, @Nullable Locale locale) {
return formatMessage(defaultMessage, args, locale); return formatMessage(defaultMessage, args, locale);
} }
@ -112,7 +112,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale used for formatting * @param locale the Locale used for formatting
* @return the formatted message (with resolved arguments) * @return the formatted message (with resolved arguments)
*/ */
protected String formatMessage(String msg, Object @Nullable [] args, Locale locale) { protected String formatMessage(String msg, Object @Nullable [] args, @Nullable Locale locale) {
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) { if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
return msg; return msg;
} }
@ -146,7 +146,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale to create a {@code MessageFormat} for * @param locale the Locale to create a {@code MessageFormat} for
* @return the {@code MessageFormat} instance * @return the {@code MessageFormat} instance
*/ */
protected MessageFormat createMessageFormat(String msg, Locale locale) { protected MessageFormat createMessageFormat(String msg, @Nullable Locale locale) {
return new MessageFormat(msg, locale); return new MessageFormat(msg, locale);
} }
@ -158,7 +158,7 @@ public abstract class MessageSourceSupport {
* @param locale the Locale to resolve against * @param locale the Locale to resolve against
* @return the resolved argument array * @return the resolved argument array
*/ */
protected Object[] resolveArguments(Object @Nullable [] args, Locale locale) { protected Object[] resolveArguments(Object @Nullable [] args, @Nullable Locale locale) {
return (args != null ? args : new Object[0]); return (args != null ? args : new Object[0]);
} }

View File

@ -26,6 +26,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.util.retry.Retry; import reactor.util.retry.Retry;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.retry.RetryException; import org.springframework.core.retry.RetryException;
@ -103,7 +104,8 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
return retryTemplate.execute(new Retryable<>() { return retryTemplate.execute(new Retryable<>() {
@Override @Override
public @Nullable Object execute() throws Throwable { public @Nullable Object execute() throws Throwable {
return invocation.proceed(); return (invocation instanceof ProxyMethodInvocation pmi ?
pmi.invocableClone().proceed() : invocation.proceed());
} }
@Override @Override
public String getName() { public String getName() {
@ -112,8 +114,7 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
}); });
} }
catch (RetryException ex) { catch (RetryException ex) {
Throwable cause = ex.getCause(); throw ex.getCause();
throw (cause != null ? cause : new IllegalStateException(ex.getMessage(), ex));
} }
} }

View File

@ -360,7 +360,7 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements
@Override @Override
public boolean isRunning() { public boolean isRunning() {
return this.triggerLifecycle.isRunning(); return (this.triggerLifecycle.isRunning() || this.fixedDelayLifecycle.isRunning());
} }
@Override @Override

View File

@ -33,6 +33,7 @@ import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver;
@ -43,7 +44,7 @@ import org.springframework.util.Assert;
* @author Mark Fisher * @author Mark Fisher
* @author Juergen Hoeller * @author Juergen Hoeller
*/ */
@Service @Lazy @DependsOn("myNamedComponent") @Service @Primary @Lazy @DependsOn("myNamedComponent")
public abstract class FooServiceImpl implements FooService { public abstract class FooServiceImpl implements FooService {
// Just to test ASM5's bytecode parsing of INVOKESPECIAL/STATIC on interfaces // Just to test ASM5's bytecode parsing of INVOKESPECIAL/STATIC on interfaces

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-present the original author 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 example.scannable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import org.springframework.context.annotation.Proxyable;
import org.springframework.stereotype.Service;
/**
* @author Juergen Hoeller
*/
@Service @Proxyable(interfaces = FooService.class)
public class OtherFooService implements FooService {
@Override
public String foo(int id) {
return "" + id;
}
@Override
public Future<String> asyncFoo(int id) {
return CompletableFuture.completedFuture("" + id);
}
@Override
public boolean isInitCalled() {
return false;
}
}

View File

@ -60,12 +60,6 @@ class CacheOperationExpressionEvaluatorTests {
private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource();
private Collection<CacheOperation> getOps(String name) {
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
return this.source.getCacheOperations(method, AnnotatedClass.class);
}
@Test @Test
void testMultipleCachingSource() { void testMultipleCachingSource() {
Collection<CacheOperation> ops = getOps("multipleCaching"); Collection<CacheOperation> ops = getOps("multipleCaching");
@ -144,6 +138,12 @@ class CacheOperationExpressionEvaluatorTests {
assertThat(value).isEqualTo(String.class.getName()); assertThat(value).isEqualTo(String.class.getName());
} }
private Collection<CacheOperation> getOps(String name) {
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
return this.source.getCacheOperations(method, AnnotatedClass.class);
}
private EvaluationContext createEvaluationContext(Object result) { private EvaluationContext createEvaluationContext(Object result) {
return createEvaluationContext(result, null); return createEvaluationContext(result, null);
} }

View File

@ -104,6 +104,7 @@ class CachePutEvaluationTests {
assertThat(this.cache.get(anotherValue + 100).get()).as("Wrong value for @CachePut key").isEqualTo(anotherValue); assertThat(this.cache.get(anotherValue + 100).get()).as("Wrong value for @CachePut key").isEqualTo(anotherValue);
} }
@Configuration @Configuration
@EnableCaching @EnableCaching
static class Config implements CachingConfigurer { static class Config implements CachingConfigurer {
@ -121,8 +122,10 @@ class CachePutEvaluationTests {
} }
@CacheConfig("test") @CacheConfig("test")
public static class SimpleService { public static class SimpleService {
private AtomicLong counter = new AtomicLong(); private AtomicLong counter = new AtomicLong();
/** /**
@ -144,4 +147,5 @@ class CachePutEvaluationTests {
return this.counter.getAndIncrement(); return this.counter.getAndIncrement();
} }
} }
} }

View File

@ -41,6 +41,7 @@ import example.scannable.JakartaNamedComponent;
import example.scannable.MessageBean; import example.scannable.MessageBean;
import example.scannable.NamedComponent; import example.scannable.NamedComponent;
import example.scannable.NamedStubDao; import example.scannable.NamedStubDao;
import example.scannable.OtherFooService;
import example.scannable.ScopedProxyTestBean; import example.scannable.ScopedProxyTestBean;
import example.scannable.ServiceInvocationCounter; import example.scannable.ServiceInvocationCounter;
import example.scannable.StubFooDao; import example.scannable.StubFooDao;
@ -85,13 +86,13 @@ class ClassPathScanningCandidateComponentProviderTests {
private static final Set<Class<?>> springComponents = Set.of( private static final Set<Class<?>> springComponents = Set.of(
DefaultNamedComponent.class, DefaultNamedComponent.class,
NamedComponent.class,
FooServiceImpl.class, FooServiceImpl.class,
StubFooDao.class, NamedComponent.class,
NamedStubDao.class, NamedStubDao.class,
OtherFooService.class,
ServiceInvocationCounter.class, ServiceInvocationCounter.class,
BarComponent.class StubFooDao.class,
); BarComponent.class);
@Test @Test
@ -213,7 +214,8 @@ class ClassPathScanningCandidateComponentProviderTests {
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertScannedBeanDefinitions(candidates); assertScannedBeanDefinitions(candidates);
// Interfaces/Abstract class are filtered out automatically. // Interfaces/Abstract class are filtered out automatically.
assertBeanTypes(candidates, AutowiredQualifierFooService.class, FooServiceImpl.class, ScopedProxyTestBean.class); assertBeanTypes(candidates,
AutowiredQualifierFooService.class, FooServiceImpl.class, OtherFooService.class, ScopedProxyTestBean.class);
} }
@Test @Test
@ -237,7 +239,8 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class)); provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertScannedBeanDefinitions(candidates); assertScannedBeanDefinitions(candidates);
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); assertBeanTypes(candidates,
NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
} }
@Test @Test
@ -282,7 +285,8 @@ class ClassPathScanningCandidateComponentProviderTests {
private void testExclude(ClassPathScanningCandidateComponentProvider provider) { private void testExclude(ClassPathScanningCandidateComponentProvider provider) {
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertScannedBeanDefinitions(candidates); assertScannedBeanDefinitions(candidates);
assertBeanTypes(candidates, FooServiceImpl.class, StubFooDao.class, ServiceInvocationCounter.class, assertBeanTypes(candidates,
FooServiceImpl.class, OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class,
BarComponent.class); BarComponent.class);
} }
@ -301,7 +305,8 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addExcludeFilter(new AnnotationTypeFilter(Service.class)); provider.addExcludeFilter(new AnnotationTypeFilter(Service.class));
provider.addExcludeFilter(new AnnotationTypeFilter(Controller.class)); provider.addExcludeFilter(new AnnotationTypeFilter(Controller.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class); assertBeanTypes(candidates,
NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
} }
@Test @Test
@ -334,8 +339,9 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); provider.addIncludeFilter(new AnnotationTypeFilter(Component.class));
provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, FooServiceImpl.class, assertBeanTypes(candidates,
BarComponent.class, DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class); DefaultNamedComponent.class, FooServiceImpl.class, NamedComponent.class, NamedStubDao.class,
OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class);
} }
@Test @Test
@ -345,8 +351,9 @@ class ClassPathScanningCandidateComponentProviderTests {
provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class)); provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class));
provider.addExcludeFilter(new AssignableTypeFilter(FooService.class)); provider.addExcludeFilter(new AssignableTypeFilter(FooService.class));
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class, assertBeanTypes(candidates,
DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class); DefaultNamedComponent.class, NamedComponent.class, NamedStubDao.class,
ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class);
} }
@Test @Test

View File

@ -47,6 +47,7 @@ class EnableAspectJAutoProxyTests {
aspectIsApplied(ctx); aspectIsApplied(ctx);
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean(FooService.class))).isTrue(); assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean(FooService.class))).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue();
ctx.close(); ctx.close();
} }
@ -56,6 +57,7 @@ class EnableAspectJAutoProxyTests {
aspectIsApplied(ctx); aspectIsApplied(ctx);
assertThat(AopUtils.isCglibProxy(ctx.getBean(FooService.class))).isTrue(); assertThat(AopUtils.isCglibProxy(ctx.getBean(FooService.class))).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue();
ctx.close(); ctx.close();
} }
@ -124,7 +126,7 @@ class EnableAspectJAutoProxyTests {
} }
@Import({ ServiceInvocationCounter.class, StubFooDao.class }) @Import({ServiceInvocationCounter.class, StubFooDao.class})
@EnableAspectJAutoProxy(exposeProxy = true) @EnableAspectJAutoProxy(exposeProxy = true)
static class ConfigWithExposedProxy { static class ConfigWithExposedProxy {

View File

@ -22,9 +22,13 @@ import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.beans.testfixture.beans.IOther;
import org.springframework.beans.testfixture.beans.ITestBean;
import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@ -32,10 +36,13 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Proxyable;
import org.springframework.context.support.GenericApplicationContext; import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.context.annotation.ProxyType.INTERFACES;
import static org.springframework.context.annotation.ProxyType.TARGET_CLASS;
/** /**
* System tests covering use of AspectJ {@link Aspect}s in conjunction with {@link Configuration} classes. * System tests covering use of AspectJ {@link Aspect}s in conjunction with {@link Configuration} classes.
@ -62,18 +69,40 @@ class ConfigurationClassAspectIntegrationTests {
assertAdviceWasApplied(ConfigurationWithAspect.class); assertAdviceWasApplied(ConfigurationWithAspect.class);
} }
private void assertAdviceWasApplied(Class<?> configClass) { @Test
void configurationIncludesAspectAndProxyable() {
assertAdviceWasApplied(ConfigurationWithAspectAndProxyable.class, TestBean.class);
}
@Test
void configurationIncludesAspectAndProxyableInterfaces() {
assertAdviceWasApplied(ConfigurationWithAspectAndProxyableInterfaces.class, TestBean.class, Comparable.class);
}
@Test
void configurationIncludesAspectAndProxyableTargetClass() {
assertAdviceWasApplied(ConfigurationWithAspectAndProxyableTargetClass.class);
}
private void assertAdviceWasApplied(Class<?> configClass, Class<?>... notImplemented) {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
new XmlBeanDefinitionReader(factory).loadBeanDefinitions( new XmlBeanDefinitionReader(factory).loadBeanDefinitions(
new ClassPathResource("aspectj-autoproxy-config.xml", ConfigurationClassAspectIntegrationTests.class)); new ClassPathResource("aspectj-autoproxy-config.xml", ConfigurationClassAspectIntegrationTests.class));
GenericApplicationContext ctx = new GenericApplicationContext(factory); GenericApplicationContext ctx = new GenericApplicationContext(factory);
ctx.addBeanFactoryPostProcessor(new ConfigurationClassPostProcessor()); ctx.addBeanFactoryPostProcessor(new ConfigurationClassPostProcessor());
ctx.registerBeanDefinition("config", new RootBeanDefinition(configClass)); ctx.registerBeanDefinition("config",
new RootBeanDefinition(configClass, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false));
ctx.refresh(); ctx.refresh();
TestBean testBean = ctx.getBean("testBean", TestBean.class); ITestBean testBean = ctx.getBean("testBean", ITestBean.class);
if (notImplemented.length > 0) {
assertThat(testBean).isNotInstanceOfAny(notImplemented);
}
else {
assertThat(testBean).isInstanceOf(TestBean.class);
}
assertThat(testBean.getName()).isEqualTo("name"); assertThat(testBean.getName()).isEqualTo("name");
testBean.absquatulate(); ((IOther) testBean).absquatulate();
assertThat(testBean.getName()).isEqualTo("advisedName"); assertThat(testBean.getName()).isEqualTo("advisedName");
ctx.close(); ctx.close();
} }
@ -120,6 +149,58 @@ class ConfigurationClassAspectIntegrationTests {
} }
@Configuration
static class ConfigurationWithAspectAndProxyable {
@Bean
@Proxyable(INTERFACES)
public TestBean testBean() {
return new TestBean("name");
}
@Bean
public NameChangingAspect nameChangingAspect() {
return new NameChangingAspect();
}
}
@Configuration()
static class ConfigurationWithAspectAndProxyableInterfaces {
@Bean
@Proxyable(interfaces = {ITestBean.class, IOther.class})
public TestBean testBean() {
return new TestBean("name");
}
@Bean
public NameChangingAspect nameChangingAspect() {
return new NameChangingAspect();
}
}
@Configuration
static class ConfigurationWithAspectAndProxyableTargetClass {
public ConfigurationWithAspectAndProxyableTargetClass(AbstractAutoProxyCreator autoProxyCreator) {
autoProxyCreator.setProxyTargetClass(false);
}
@Bean
@Proxyable(TARGET_CLASS)
public TestBean testBean() {
return new TestBean("name");
}
@Bean
public NameChangingAspect nameChangingAspect() {
return new NameChangingAspect();
}
}
@Aspect @Aspect
static class NameChangingAspect { static class NameChangingAspect {

View File

@ -355,6 +355,7 @@ class DefaultLifecycleProcessorTests {
TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forShutdownTests(5, 0, stoppedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forShutdownTests(5, 0, stoppedBeans);
TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forShutdownTests(-3, 0, stoppedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forShutdownTests(-3, 0, stoppedBeans);
smartBean2.setAutoStartup(false); smartBean2.setAutoStartup(false);
smartBean2.setPauseable(false);
context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1);
context.getBeanFactory().registerSingleton("smartBean2", smartBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2);
@ -375,11 +376,23 @@ class DefaultLifecycleProcessorTests {
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1); assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1);
assertThat(smartBean1.isRunning()).isTrue(); assertThat(smartBean1.isRunning()).isTrue();
assertThat(smartBean2.isRunning()).isFalse(); assertThat(smartBean2.isRunning()).isFalse();
context.pause();
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1);
assertThat(smartBean1.isRunning()).isFalse();
assertThat(smartBean2.isRunning()).isFalse();
context.restart();
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1);
assertThat(smartBean1.isRunning()).isTrue();
assertThat(smartBean2.isRunning()).isFalse();
context.start(); context.start();
assertThat(smartBean1.isRunning()).isTrue(); assertThat(smartBean1.isRunning()).isTrue();
assertThat(smartBean2.isRunning()).isTrue(); assertThat(smartBean2.isRunning()).isTrue();
context.pause();
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean1);
assertThat(smartBean1.isRunning()).isFalse();
assertThat(smartBean2.isRunning()).isTrue();
context.close(); context.close();
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean2); assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean1, smartBean2);
} }
@Test @Test
@ -740,6 +753,8 @@ class DefaultLifecycleProcessorTests {
private volatile boolean autoStartup = true; private volatile boolean autoStartup = true;
private volatile boolean pauseable = true;
static TestSmartLifecycleBean forStartupTests(int phase, CopyOnWriteArrayList<Lifecycle> startedBeans) { static TestSmartLifecycleBean forStartupTests(int phase, CopyOnWriteArrayList<Lifecycle> startedBeans) {
return new TestSmartLifecycleBean(phase, 0, startedBeans, null); return new TestSmartLifecycleBean(phase, 0, startedBeans, null);
} }
@ -769,6 +784,15 @@ class DefaultLifecycleProcessorTests {
this.autoStartup = autoStartup; this.autoStartup = autoStartup;
} }
@Override
public boolean isPauseable() {
return this.pauseable;
}
public void setPauseable(boolean pauseable) {
this.pauseable = pauseable;
}
@Override @Override
public void stop(final Runnable callback) { public void stop(final Runnable callback) {
// calling stop() before the delay to preserve // calling stop() before the delay to preserve

View File

@ -189,6 +189,26 @@ class ReactiveRetryInterceptorTests {
assertThat(target.counter.get()).isEqualTo(2); assertThat(target.counter.get()).isEqualTo(2);
} }
@Test
void adaptReactiveResultWithZeroAttempts() {
// Test minimal retry configuration: maxAttempts=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
MinimalRetryBean target = new MinimalRetryBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvice(new SimpleRetryInterceptor(
new MethodRetrySpec((m, t) -> true, 0, Duration.ZERO, Duration.ZERO, 1.0, Duration.ZERO)));
MinimalRetryBean proxy = (MinimalRetryBean) pf.getProxy();
// Should execute only 1 time, because maxAttempts=0 means initial call only
assertThatIllegalStateException()
.isThrownBy(() -> proxy.retryOperation().block())
.satisfies(isRetryExhaustedException())
.havingCause()
.isInstanceOf(IOException.class)
.withMessage("1");
assertThat(target.counter.get()).isEqualTo(1);
}
@Test @Test
void adaptReactiveResultWithZeroDelayAndJitter() { void adaptReactiveResultWithZeroDelayAndJitter() {
// Test case where delay=0 and jitter>0 // Test case where delay=0 and jitter>0

View File

@ -17,16 +17,22 @@
package org.springframework.resilience; package org.springframework.resilience;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
import java.time.Duration; import java.time.Duration;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.framework.ProxyConfig;
import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.aop.interceptor.SimpleTraceInterceptor;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@ -56,7 +62,30 @@ class RetryInterceptorTests {
pf.setTarget(target); pf.setTarget(target);
pf.addAdvice(new SimpleRetryInterceptor( pf.addAdvice(new SimpleRetryInterceptor(
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10)))); new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy(); pf.addAdvice(new SimpleTraceInterceptor());
PlainInterface proxy = (PlainInterface) pf.getProxy();
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
assertThat(target.counter).isEqualTo(6);
}
@Test
void withSimpleInterceptorAndNoTarget() {
NonAnnotatedBean target = new NonAnnotatedBean();
ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new SimpleRetryInterceptor(
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
pf.addAdvice(new SimpleTraceInterceptor());
pf.addAdvice((MethodInterceptor) invocation -> {
try {
return invocation.getMethod().invoke(target, invocation.getArguments());
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
});
pf.addInterface(PlainInterface.class);
PlainInterface proxy = (PlainInterface) pf.getProxy();
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6"); assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
assertThat(target.counter).isEqualTo(6); assertThat(target.counter).isEqualTo(6);
@ -76,6 +105,78 @@ class RetryInterceptorTests {
assertThat(target.counter).isEqualTo(6); assertThat(target.counter).isEqualTo(6);
} }
@Test
void withPostProcessorForMethodWithInterface() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
bpp.setBeanFactory(bf);
bf.addBeanPostProcessor(bpp);
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
assertThat(target.counter).isEqualTo(6);
}
@Test
void withPostProcessorForMethodWithInterfaceAndDefaultTargetClass() {
ProxyConfig defaultProxyConfig = new ProxyConfig();
defaultProxyConfig.setProxyTargetClass(true);
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig);
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
bpp.setBeanFactory(bf);
bf.addBeanPostProcessor(bpp);
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
assertThat(target.counter).isEqualTo(6);
}
@Test
void withPostProcessorForMethodWithInterfaceAndPreserveTargetClass() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class);
bd.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
bf.registerBeanDefinition("bean", bd);
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
bpp.setBeanFactory(bf);
bf.addBeanPostProcessor(bpp);
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
assertThat(target.counter).isEqualTo(6);
}
@Test
void withPostProcessorForMethodWithInterfaceAndExposeInterfaces() {
ProxyConfig defaultProxyConfig = new ProxyConfig();
defaultProxyConfig.setProxyTargetClass(true);
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig);
RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class);
bd.setAttribute(AutoProxyUtils.EXPOSED_INTERFACES_ATTRIBUTE, AutoProxyUtils.ALL_INTERFACES_ATTRIBUTE_VALUE);
bf.registerBeanDefinition("bean", bd);
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
bpp.setBeanFactory(bf);
bf.addBeanPostProcessor(bpp);
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
assertThat(target.counter).isEqualTo(6);
}
@Test @Test
void withPostProcessorForClass() { void withPostProcessorForClass() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
@ -119,6 +220,31 @@ class RetryInterceptorTests {
assertThat(target.counter).isEqualTo(6); assertThat(target.counter).isEqualTo(6);
} }
@Test
void withPostProcessorForClassWithZeroAttempts() {
Properties props = new Properties();
props.setProperty("delay", "10");
props.setProperty("jitter", "5");
props.setProperty("multiplier", "2.0");
props.setProperty("maxDelay", "40");
props.setProperty("limitedAttempts", "0");
GenericApplicationContext ctx = new GenericApplicationContext();
ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("props", props));
ctx.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBeanWithStrings.class));
ctx.registerBeanDefinition("bpp", new RootBeanDefinition(RetryAnnotationBeanPostProcessor.class));
ctx.refresh();
AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class);
AnnotatedClassBeanWithStrings target = (AnnotatedClassBeanWithStrings) AopProxyUtils.getSingletonTarget(proxy);
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3");
assertThat(target.counter).isEqualTo(3);
assertThatIOException().isThrownBy(proxy::otherOperation);
assertThat(target.counter).isEqualTo(4);
assertThatIOException().isThrownBy(proxy::overrideOperation);
assertThat(target.counter).isEqualTo(5);
}
@Test @Test
void withEnableAnnotation() throws Exception { void withEnableAnnotation() throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
@ -137,7 +263,7 @@ class RetryInterceptorTests {
} }
static class NonAnnotatedBean { static class NonAnnotatedBean implements PlainInterface {
int counter = 0; int counter = 0;
@ -148,6 +274,12 @@ class RetryInterceptorTests {
} }
public interface PlainInterface {
void retryOperation() throws IOException;
}
static class AnnotatedMethodBean { static class AnnotatedMethodBean {
int counter = 0; int counter = 0;
@ -160,6 +292,26 @@ class RetryInterceptorTests {
} }
static class AnnotatedMethodBeanWithInterface implements AnnotatedInterface {
int counter = 0;
@Retryable(maxAttempts = 5, delay = 10)
@Override
public void retryOperation() throws IOException {
counter++;
throw new IOException(Integer.toString(counter));
}
}
interface AnnotatedInterface {
@Retryable(maxAttempts = 5, delay = 10)
void retryOperation() throws IOException;
}
@Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40, @Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40,
includes = IOException.class, excludes = AccessDeniedException.class, includes = IOException.class, excludes = AccessDeniedException.class,
predicate = CustomPredicate.class) predicate = CustomPredicate.class)

View File

@ -1,12 +1,13 @@
example.scannable.AutowiredQualifierFooService=example.scannable.FooService example.scannable.AutowiredQualifierFooService=example.scannable.FooService
example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component
example.scannable.NamedComponent=org.springframework.stereotype.Component
example.scannable.FooService=example.scannable.FooService example.scannable.FooService=example.scannable.FooService
example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService
example.scannable.ScopedProxyTestBean=example.scannable.FooService example.scannable.NamedComponent=org.springframework.stereotype.Component
example.scannable.StubFooDao=org.springframework.stereotype.Component
example.scannable.NamedStubDao=org.springframework.stereotype.Component example.scannable.NamedStubDao=org.springframework.stereotype.Component
example.scannable.OtherFooService=org.springframework.stereotype.Component,example.scannable.FooService
example.scannable.ScopedProxyTestBean=example.scannable.FooService
example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component
example.scannable.StubFooDao=org.springframework.stereotype.Component
example.scannable.sub.BarComponent=org.springframework.stereotype.Component example.scannable.sub.BarComponent=org.springframework.stereotype.Component
example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean
example.scannable.JakartaNamedComponent=jakarta.inject.Named example.scannable.JakartaNamedComponent=jakarta.inject.Named

View File

@ -14,7 +14,7 @@ multiRelease {
releaseVersions 21, 24 releaseVersions 21, 24
} }
def javapoetVersion = "1.13.0" def javapoetVersion = "0.7.0"
def objenesisVersion = "3.4" def objenesisVersion = "3.4"
configurations { configurations {
@ -30,12 +30,12 @@ tasks.register('javapoetRepackJar', ShadowJar) {
archiveBaseName = 'spring-javapoet-repack' archiveBaseName = 'spring-javapoet-repack'
archiveVersion = javapoetVersion archiveVersion = javapoetVersion
configurations = [project.configurations.javapoet] configurations = [project.configurations.javapoet]
relocate('com.squareup.javapoet', 'org.springframework.javapoet') relocate('com.palantir.javapoet', 'org.springframework.javapoet')
} }
tasks.register('javapoetSource', ShadowSource) { tasks.register('javapoetSource', ShadowSource) {
configurations = [project.configurations.javapoet] configurations = [project.configurations.javapoet]
relocate('com.squareup.javapoet', 'org.springframework.javapoet') relocate('com.palantir.javapoet', 'org.springframework.javapoet')
outputDirectory = file("build/shadow-source/javapoet") outputDirectory = file("build/shadow-source/javapoet")
} }
@ -67,7 +67,7 @@ tasks.register('objenesisSourceJar', Jar) {
} }
dependencies { dependencies {
javapoet("com.squareup:javapoet:${javapoetVersion}@jar") javapoet("com.palantir.javapoet:javapoet:${javapoetVersion}@jar")
objenesis("org.objenesis:objenesis:${objenesisVersion}@jar") objenesis("org.objenesis:objenesis:${objenesisVersion}@jar")
api(files(javapoetRepackJar)) api(files(javapoetRepackJar))
api(files(objenesisRepackJar)) api(files(objenesisRepackJar))
@ -96,7 +96,7 @@ dependencies {
testImplementation("com.fasterxml.jackson.core:jackson-databind") testImplementation("com.fasterxml.jackson.core:jackson-databind")
testImplementation("com.fasterxml.woodstox:woodstox-core") testImplementation("com.fasterxml.woodstox:woodstox-core")
testImplementation("com.google.code.findbugs:jsr305") testImplementation("com.google.code.findbugs:jsr305")
testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("com.squareup.okhttp3:mockwebserver3")
testImplementation("io.projectreactor:reactor-test") testImplementation("io.projectreactor:reactor-test")
testImplementation("io.projectreactor.tools:blockhound") testImplementation("io.projectreactor.tools:blockhound")
testImplementation("jakarta.annotation:jakarta.annotation-api") testImplementation("jakarta.annotation:jakarta.annotation-api")

View File

@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.javapoet.ClassName; import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterSpec;
import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeName;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -51,7 +52,7 @@ public class DefaultMethodReference implements MethodReference {
@Override @Override
public CodeBlock toCodeBlock() { public CodeBlock toCodeBlock() {
String methodName = this.method.name; String methodName = this.method.name();
if (isStatic()) { if (isStatic()) {
Assert.state(this.declaringClass != null, "Static method reference must define a declaring class"); Assert.state(this.declaringClass != null, "Static method reference must define a declaring class");
return CodeBlock.of("$T::$L", this.declaringClass, methodName); return CodeBlock.of("$T::$L", this.declaringClass, methodName);
@ -65,7 +66,7 @@ public class DefaultMethodReference implements MethodReference {
public CodeBlock toInvokeCodeBlock(ArgumentCodeGenerator argumentCodeGenerator, public CodeBlock toInvokeCodeBlock(ArgumentCodeGenerator argumentCodeGenerator,
@Nullable ClassName targetClassName) { @Nullable ClassName targetClassName) {
String methodName = this.method.name; String methodName = this.method.name();
CodeBlock.Builder code = CodeBlock.builder(); CodeBlock.Builder code = CodeBlock.builder();
if (isStatic()) { if (isStatic()) {
Assert.state(this.declaringClass != null, "Static method reference must define a declaring class"); Assert.state(this.declaringClass != null, "Static method reference must define a declaring class");
@ -96,8 +97,8 @@ public class DefaultMethodReference implements MethodReference {
*/ */
protected void addArguments(CodeBlock.Builder code, ArgumentCodeGenerator argumentCodeGenerator) { protected void addArguments(CodeBlock.Builder code, ArgumentCodeGenerator argumentCodeGenerator) {
List<CodeBlock> arguments = new ArrayList<>(); List<CodeBlock> arguments = new ArrayList<>();
TypeName[] argumentTypes = this.method.parameters.stream() TypeName[] argumentTypes = this.method.parameters().stream()
.map(parameter -> parameter.type).toArray(TypeName[]::new); .map(ParameterSpec::type).toArray(TypeName[]::new);
for (int i = 0; i < argumentTypes.length; i++) { for (int i = 0; i < argumentTypes.length; i++) {
TypeName argumentType = argumentTypes[i]; TypeName argumentType = argumentTypes[i];
CodeBlock argumentCode = argumentCodeGenerator.generateCode(argumentType); CodeBlock argumentCode = argumentCodeGenerator.generateCode(argumentType);
@ -115,12 +116,12 @@ public class DefaultMethodReference implements MethodReference {
} }
private boolean isStatic() { private boolean isStatic() {
return this.method.modifiers.contains(Modifier.STATIC); return this.method.modifiers().contains(Modifier.STATIC);
} }
@Override @Override
public String toString() { public String toString() {
String methodName = this.method.name; String methodName = this.method.name();
if (isStatic()) { if (isStatic()) {
return this.declaringClass + "::" + methodName; return this.declaringClass + "::" + methodName;
} }

View File

@ -48,8 +48,8 @@ public interface GeneratedFiles {
* @param javaFile the java file to add * @param javaFile the java file to add
*/ */
default void addSourceFile(JavaFile javaFile) { default void addSourceFile(JavaFile javaFile) {
validatePackage(javaFile.packageName, javaFile.typeSpec.name); validatePackage(javaFile.packageName(), javaFile.typeSpec().name());
String className = javaFile.packageName + "." + javaFile.typeSpec.name; String className = javaFile.packageName() + "." + javaFile.typeSpec().name();
addSourceFile(className, javaFile::writeTo); addSourceFile(className, javaFile::writeTo);
} }

View File

@ -53,7 +53,7 @@ public final class GeneratedMethod {
MethodSpec.Builder builder = MethodSpec.methodBuilder(this.name); MethodSpec.Builder builder = MethodSpec.methodBuilder(this.name);
method.accept(builder); method.accept(builder);
this.methodSpec = builder.build(); this.methodSpec = builder.build();
Assert.state(this.name.equals(this.methodSpec.name), Assert.state(this.name.equals(this.methodSpec.name()),
"'method' consumer must not change the generated method name"); "'method' consumer must not change the generated method name");
} }

View File

@ -202,7 +202,7 @@ public class AnnotatedMethod {
} }
for (int i = 0; i < paramTypes.length; i++) { for (int i = 0; i < paramTypes.length; i++) {
if (paramTypes[i] != if (paramTypes[i] !=
ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) { ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).toClass()) {
return false; return false;
} }
} }

View File

@ -374,7 +374,7 @@ abstract class AnnotationsScanner {
} }
for (int i = 0; i < rootParameterTypes.length; i++) { for (int i = 0; i < rootParameterTypes.length; i++) {
Class<?> resolvedParameterType = ResolvableType.forMethodParameter( Class<?> resolvedParameterType = ResolvableType.forMethodParameter(
candidateMethod, i, sourceDeclaringClass).resolve(); candidateMethod, i, sourceDeclaringClass).toClass();
if (rootParameterTypes[i] != resolvedParameterType) { if (rootParameterTypes[i] != resolvedParameterType) {
return false; return false;
} }

View File

@ -29,7 +29,6 @@ import java.util.function.Predicate;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
/** /**
@ -483,7 +482,7 @@ final class TypeMappedAnnotations implements MergedAnnotations {
} }
@Override @Override
public @NonNull List<Aggregate> finish(@Nullable List<Aggregate> processResult) { public List<Aggregate> finish(@Nullable List<Aggregate> processResult) {
return this.aggregates; return this.aggregates;
} }
} }

View File

@ -361,12 +361,22 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
* @throws IOException if thrown from URLConnection methods * @throws IOException if thrown from URLConnection methods
*/ */
protected void customizeConnection(URLConnection con) throws IOException { protected void customizeConnection(URLConnection con) throws IOException {
ResourceUtils.useCachesIfNecessary(con); useCachesIfNecessary(con);
if (con instanceof HttpURLConnection httpCon) { if (con instanceof HttpURLConnection httpCon) {
customizeConnection(httpCon); customizeConnection(httpCon);
} }
} }
/**
* Apply {@link URLConnection#setUseCaches useCaches} if necessary.
* @param con the URLConnection to customize
* @since 6.2.10
* @see ResourceUtils#useCachesIfNecessary(URLConnection)
*/
void useCachesIfNecessary(URLConnection con) {
ResourceUtils.useCachesIfNecessary(con);
}
/** /**
* Customize the given {@link HttpURLConnection} before fetching the resource. * Customize the given {@link HttpURLConnection} before fetching the resource.
* <p>Can be overridden in subclasses for configuring request headers and timeouts. * <p>Can be overridden in subclasses for configuring request headers and timeouts.

View File

@ -109,7 +109,9 @@ public class FileUrlResource extends UrlResource implements WritableResource {
@Override @Override
public Resource createRelative(String relativePath) throws MalformedURLException { public Resource createRelative(String relativePath) throws MalformedURLException {
return new FileUrlResource(createRelativeURL(relativePath)); FileUrlResource resource = new FileUrlResource(createRelativeURL(relativePath));
resource.useCaches = this.useCaches;
return resource;
} }
} }

Some files were not shown because too many files have changed in this diff Show More