Merge branch 'main' into 35179
This commit is contained in:
commit
832689925c
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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/",
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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/"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
|
||||||
----
|
----
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
====
|
====
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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[]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
----
|
||||||
|
======
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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].
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`.
|
|
@ -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]
|
||||||
======
|
======
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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]]
|
||||||
|
|
|
@ -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`.
|
|
@ -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
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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/"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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 {};
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue