diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 9eaafdc2da31..1e486fe672c8 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -19,7 +19,7 @@ inputs: java-version: description: 'Java version to compile and test with' required: false - default: '24' + default: '25' publish: description: 'Whether to publish artifacts ready for deployment to Artifactory' required: false diff --git a/.github/actions/prepare-gradle-build/action.yml b/.github/actions/prepare-gradle-build/action.yml index c07a74c1dfef..99f5f23cff13 100644 --- a/.github/actions/prepare-gradle-build/action.yml +++ b/.github/actions/prepare-gradle-build/action.yml @@ -19,7 +19,7 @@ inputs: java-version: description: 'Java version to use for the build' required: false - default: '24' + default: '25' runs: using: composite steps: @@ -30,6 +30,7 @@ runs: java-version: | ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} ${{ inputs.java-toolchain == 'true' && '17' || '' }} + 25 - name: Set Up Gradle uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1 with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bf511a2827f..f1fa7b826222 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: toolchain: false - version: 21 toolchain: true - - version: 24 + - version: 25 toolchain: true exclude: - os: diff --git a/.sdkmanrc b/.sdkmanrc index b41ba343b002..2b4236b43e3c 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=24.0.1-librca +java=25-librca diff --git a/README.md b/README.md index 9dd6a0f15954..edd478f0d829 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ See the [Build from Source](https://github.com/spring-projects/spring-framework/ ## Continuous Integration Builds -Information regarding CI builds can be found in the [Spring Framework Concourse pipeline](ci/README.adoc) documentation. +CI builds are defined with [GitHub Actions workflows](.github/workflows). ## Stay in Touch diff --git a/build.gradle b/build.gradle index 6fafc96e97f3..a86c93fcd8a6 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ plugins { id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false id 'org.jetbrains.dokka' id 'com.github.bjornvester.xjc' version '1.8.2' apply false - id 'io.github.goooler.shadow' version '8.1.8' apply false + id 'com.gradleup.shadow' version "9.2.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.8' apply false } ext { @@ -69,23 +69,16 @@ configure([rootProject] + javaProjects) { project -> ext.javadocLinks = [ "https://docs.oracle.com/en/java/javase/17/docs/api/", - "https://jakarta.ee/specifications/platform/11/apidocs/", - "https://docs.jboss.org/hibernate/orm/5.6/javadocs/", + //"https://jakarta.ee/specifications/platform/11/apidocs/", + "https://docs.hibernate.org/orm/5.6/javadocs/", "https://www.quartz-scheduler.org/api/2.3.0/", "https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/", "https://projectreactor.io/docs/test/release/api/", "https://junit.org/junit4/javadoc/4.13.2/", - "https://docs.junit.org/5.13.3/api/", + "https://docs.junit.org/6.0.1/api/", "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/", - //"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/", "https://r2dbc.io/spec/1.0.0.RELEASE/api/", - // Previously there could be a split-package issue between JSR250 and JSR305 javax.annotation packages, - // but since 6.0 JSR 250 annotations such as @Resource and @PostConstruct have been replaced by their - // JakartaEE equivalents in the jakarta.annotation package. - //"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/", - "https://jspecify.dev/docs/api/", - "https://www.javadoc.io/doc/tools.jackson.core/jackson-databind/3.0.0-rc4/" - + "https://jspecify.dev/docs/api/" ] as String[] } diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index ed471b72eae6..58cbf888cd4b 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -20,8 +20,8 @@ ext { dependencies { checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}" 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 "org.jetbrains.dokka:dokka-gradle-plugin:2.1.0" + implementation "com.tngtech.archunit:archunit:1.4.1" implementation "org.gradle:test-retry-gradle-plugin:1.6.2" implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}" implementation "io.spring.nohttp:nohttp-gradle:0.0.11" diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index 19a06771cdf1..9f235e4991c0 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.26.1"); + checkstyle.setToolVersion("12.1.1"); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); diff --git a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java index 52add8eb7804..741999760f23 100644 --- a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java @@ -45,7 +45,7 @@ public class JavaConventions { *

NOTE: If you update this value, you should also update the value used in * the {@code javadoc} task in {@code framework-api.gradle}. */ - private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(24); + private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(25); /** * The Java version we should use as the baseline for the compiled bytecode diff --git a/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java index 9e9c871d05cf..a2aa7fba46c7 100644 --- a/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java @@ -27,6 +27,10 @@ import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.AbstractArchiveTask; +import org.gradle.jvm.tasks.Jar; +import org.gradle.jvm.toolchain.JavaLanguageVersion; import org.gradle.jvm.toolchain.JavaToolchainService; /** @@ -38,6 +42,8 @@ */ public class MultiReleaseJarPlugin implements Plugin { + public static String VALIDATE_JAR_TASK_NAME = "validateMultiReleaseJar"; + @Inject protected JavaToolchainService getToolchains() { throw new UnsupportedOperationException(); @@ -57,5 +63,11 @@ public void apply(Project project) { tasks, dependencies, objects); + + TaskProvider validateJarTask = tasks.register(VALIDATE_JAR_TASK_NAME, MultiReleaseJarValidateTask.class, (task) -> { + task.getJar().set(tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile)); + task.getJavaLauncher().set(task.getJavaToolchainService().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(25)))); + }); + tasks.named("check", task -> task.dependsOn(validateJarTask)); } } diff --git a/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarValidateTask.java b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarValidateTask.java new file mode 100644 index 000000000000..fd1e49606499 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarValidateTask.java @@ -0,0 +1,47 @@ +/* + * 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.build.multirelease; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.CacheableTask; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.JavaExec; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.jvm.toolchain.JavaToolchainService; + +import java.util.List; + +import javax.inject.Inject; + +@CacheableTask +public abstract class MultiReleaseJarValidateTask extends JavaExec { + + + public MultiReleaseJarValidateTask() { + getMainModule().set("jdk.jartool"); + getArgumentProviders().add(() -> List.of("--validate", "--file", getJar().get().getAsFile().getAbsolutePath())); + } + + @Inject + protected abstract JavaToolchainService getJavaToolchainService(); + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + public abstract RegularFileProperty getJar(); + +} diff --git a/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java index a678d4506d45..58d47e82e124 100644 --- a/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java @@ -26,11 +26,13 @@ import java.util.jar.JarFile; import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.GradleRunner; +import org.gradle.testkit.runner.UnexpectedBuildFailure; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link MultiReleaseJarPlugin} @@ -116,6 +118,34 @@ public class Main {} } } + @Test + void validateJar() throws IOException { + writeBuildFile(""" + plugins { + id 'java' + id 'org.springframework.build.multiReleaseJar' + } + version = '1.2.3' + tasks.withType(JavaCompile).configureEach { + options.release = 11 + } + multiRelease { releaseVersions 17 } + """); + writeClass("src/main/java17", "Main.java", """ + public class Main { + + public void method() {} + + } + """); + writeClass("src/main/java", "Main.java", """ + public class Main {} + """); + assertThatThrownBy(() ->runGradle("validateMultiReleaseJar")) + .isInstanceOf(UnexpectedBuildFailure.class) + .hasMessageContaining("entry: META-INF/versions/17/Main.class, contains a class with different api from earlier version"); + } + private void writeBuildFile(String buildContent) throws IOException { try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) { out.print(buildContent); diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index fc3ecb8637f1..978ff802e6d6 100644 --- a/framework-api/framework-api.gradle +++ b/framework-api/framework-api.gradle @@ -22,7 +22,7 @@ dependencies { javadoc { javadocTool.set(javaToolchains.javadocToolFor({ - languageVersion = JavaLanguageVersion.of(24) + languageVersion = JavaLanguageVersion.of(25) })) title = "${rootProject.description} ${version} API" diff --git a/framework-docs/framework-docs.gradle b/framework-docs/framework-docs.gradle index 89250dcf590b..1ba931790dc6 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -1,3 +1,4 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask plugins { @@ -43,10 +44,14 @@ repositories { } } -// To avoid a redeclaration error with Kotlin compiler -tasks.named('compileKotlin', KotlinCompilationTask.class) { +// To avoid a redeclaration error with Kotlin compiler and set the JVM target +tasks.withType(KotlinCompilationTask.class).configureEach { javaSources.from = [] - compilerOptions.freeCompilerArgs = [ "-Xannotation-default-target=param-property" ] // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255 + compilerOptions.jvmTarget = JvmTarget.JVM_17 + compilerOptions.freeCompilerArgs.addAll( + "-Xjdk-release=17", // Needed due to https://youtrack.jetbrains.com/issue/KT-49746 + "-Xannotation-default-target=param-property" // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255 + ) } dependencies { @@ -65,6 +70,7 @@ dependencies { implementation("com.github.ben-manes.caffeine:caffeine") implementation("com.mchange:c3p0:0.9.5.5") implementation("com.oracle.database.jdbc:ojdbc11") + implementation("io.micrometer:context-propagation") implementation("io.projectreactor.netty:reactor-netty-http") implementation("jakarta.jms:jakarta.jms-api") implementation("jakarta.servlet:jakarta.servlet-api") @@ -78,6 +84,8 @@ dependencies { implementation("org.assertj:assertj-core") implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") implementation("org.jetbrains.kotlin:kotlin-stdlib") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") implementation("org.junit.jupiter:junit-jupiter-api") implementation("tools.jackson.core:jackson-databind") implementation("tools.jackson.dataformat:jackson-dataformat-xml") diff --git a/framework-docs/modules/ROOT/nav.adoc b/framework-docs/modules/ROOT/nav.adoc index 7d8e6903e15f..e51cc221fa36 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -314,7 +314,7 @@ *** xref:web/webflux-webclient/client-context.adoc[] *** xref:web/webflux-webclient/client-synchronous.adoc[] *** xref:web/webflux-webclient/client-testing.adoc[] -** xref:web/webflux-http-interface-client.adoc[] +** xref:web/webflux-http-service-client.adoc[] ** xref:web/webflux-websocket.adoc[] ** xref:web/webflux-test.adoc[] ** xref:rsocket.adoc[] @@ -355,6 +355,7 @@ *** xref:testing/testcontext-framework/support-classes.adoc[] *** xref:testing/testcontext-framework/aot.adoc[] ** xref:testing/webtestclient.adoc[] +** xref:testing/resttestclient.adoc[] ** xref:testing/mockmvc.adoc[] *** xref:testing/mockmvc/overview.adoc[] *** xref:testing/mockmvc/setup-options.adoc[] @@ -448,14 +449,13 @@ *** xref:languages/kotlin/null-safety.adoc[] *** xref:languages/kotlin/classes-interfaces.adoc[] *** xref:languages/kotlin/annotations.adoc[] -*** xref:languages/kotlin/bean-definition-dsl.adoc[] +*** xref:languages/kotlin/bean-registration-dsl.adoc[] *** xref:languages/kotlin/web.adoc[] *** xref:languages/kotlin/coroutines.adoc[] *** xref:languages/kotlin/spring-projects-in.adoc[] *** xref:languages/kotlin/getting-started.adoc[] *** xref:languages/kotlin/resources.adoc[] ** xref:languages/groovy.adoc[] -** xref:languages/dynamic.adoc[] * xref:appendix.adoc[] * {spring-framework-docs-root}/{spring-version}/javadoc-api/[Java API,window=_blank, role=link-external] * {spring-framework-api-kdoc}/[Kotlin API,window=_blank, role=link-external] diff --git a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc index 4a01901853c1..b69cac9f570a 100644 --- a/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop-api/targetsource.adoc @@ -87,14 +87,12 @@ A crucial difference between Spring pooling and SLSB pooling is that Spring pool be applied to any POJO. As with Spring in general, this service can be applied in a non-invasive way. -Spring provides support for Commons Pool 2.2, which provides a +Spring provides support for Commons Pool 2, which provides a fairly efficient pooling implementation. You need the `commons-pool` Jar on your application's classpath to use this feature. You can also subclass `org.springframework.aop.target.AbstractPoolingTargetSource` to support any other pooling API. -NOTE: Commons Pool 1.5+ is also supported but is deprecated as of Spring Framework 4.2. - The following listing shows an example configuration: [source,xml,indent=0,subs="verbatim,quotes"] diff --git a/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc b/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc index 58d150a4c8d2..429e5d6e7ef1 100644 --- a/framework-docs/modules/ROOT/pages/core/aop/proxying.adoc +++ b/framework-docs/modules/ROOT/pages/core/aop/proxying.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 `--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 of the `` element to true, as follows: @@ -60,6 +64,24 @@ To be clear, using `proxy-target-class="true"` on ``, 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]] == Understanding AOP Proxies diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc index c108047ec6cf..a821a5b8a690 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/autowired.adoc @@ -37,18 +37,18 @@ Kotlin:: ---- ====== -[NOTE] +[TIP] ==== -As of Spring Framework 4.3, an `@Autowired` annotation on such a constructor is no longer -necessary if the target bean defines only one constructor to begin with. However, if -several constructors are available and there is no primary/default constructor, at least -one of the constructors must be annotated with `@Autowired` in order to instruct the -container which one to use. See the discussion on -xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution] for details. +An `@Autowired` annotation on such a constructor is not necessary if the target bean +defines only one constructor. However, if several constructors are available and there is +no primary or default constructor, at least one of the constructors must be annotated +with `@Autowired` in order to instruct the container which one to use. See the discussion +on xref:core/beans/annotation-config/autowired.adoc#beans-autowired-annotation-constructor-resolution[constructor resolution] +for details. ==== -You can also apply the `@Autowired` annotation to _traditional_ setter methods, -as the following example shows: +You can apply the `@Autowired` annotation to _traditional_ setter methods, as the +following example shows: [tabs] ====== @@ -84,8 +84,8 @@ Kotlin:: ---- ====== -You can also apply the annotation to methods with arbitrary names and multiple -arguments, as the following example shows: +You can apply `@Autowired` to methods with arbitrary names and multiple arguments, as the +following example shows: [tabs] ====== @@ -176,14 +176,15 @@ Kotlin:: ==== Make sure that your target components (for example, `MovieCatalog` or `CustomerPreferenceDao`) are consistently declared by the type that you use for your `@Autowired`-annotated -injection points. Otherwise, injection may fail due to a "no type match found" error at runtime. +injection points. Otherwise, injection may fail due to a "no type match found" error at +runtime. For XML-defined beans or component classes found via classpath scanning, the container usually knows the concrete type up front. However, for `@Bean` factory methods, you need to make sure that the declared return type is sufficiently expressive. For components that implement several interfaces or for components potentially referred to by their -implementation type, consider declaring the most specific return type on your factory -method (at least as specific as required by the injection points referring to your bean). +implementation type, declare the most specific return type on your factory method (at +least as specific as required by the injection points referring to your bean). ==== .[[beans-autowired-annotation-self-injection]]Self Injection @@ -312,8 +313,8 @@ through `@Order` values in combination with `@Primary` on a single bean for each ==== Even typed `Map` instances can be autowired as long as the expected key type is `String`. -The map values contain all beans of the expected type, and the keys contain the -corresponding bean names, as the following example shows: +The map values are all beans of the expected type, and the keys are the corresponding +bean names, as the following example shows: [tabs] ====== @@ -431,7 +432,7 @@ annotated constructor does not have to be public. ==== Alternatively, you can express the non-required nature of a particular dependency -through Java 8's `java.util.Optional`, as the following example shows: +through Java's `java.util.Optional`, as the following example shows: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -445,8 +446,8 @@ through Java 8's `java.util.Optional`, as the following example shows: ---- You can also use a parameter-level `@Nullable` annotation (of any kind in any package -- -for example, `javax.annotation.Nullable` from JSR-305) or just leverage Kotlin built-in -null-safety support: +for example, `org.jspecify.annotations.Nullable` from JSpecify) or just leverage Kotlin's +built-in null-safety support: [tabs] ====== @@ -477,13 +478,6 @@ Kotlin:: ---- ====== -[NOTE] -==== -A type-level `@Nullable` annotation such as from JSpecify is not supported in Spring -Framework 6.2 yet. You need to upgrade to Spring Framework 7.0 where the framework -detects type-level annotations and consistently declares JSpecify in its own codebase. -==== - You can also use `@Autowired` for interfaces that are well-known resolvable dependencies: `BeanFactory`, `ApplicationContext`, `Environment`, `ResourceLoader`, `ApplicationEventPublisher`, and `MessageSource`. These interfaces and their extended @@ -528,5 +522,6 @@ class MovieRecommender { The `@Autowired`, `@Inject`, `@Value`, and `@Resource` annotations are handled by Spring `BeanPostProcessor` implementations. This means that you cannot apply these annotations within your own `BeanPostProcessor` or `BeanFactoryPostProcessor` types (if any). + These types must be 'wired up' explicitly by using XML or a Spring `@Bean` method. ==== diff --git a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc index 25217f97f65e..ea07e2be83df 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/basics.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/basics.adoc @@ -239,40 +239,6 @@ The namespace itself provides the import directive feature. Further configuration features beyond plain bean definitions are available in a selection of XML namespaces provided by Spring -- for example, the `context` and `util` namespaces. -[[beans-factory-groovy]] -=== The Groovy Bean Definition DSL - -As a further example for externalized configuration metadata, bean definitions can also -be expressed in Spring's Groovy Bean Definition DSL, as known from the Grails framework. -Typically, such configuration live in a ".groovy" file with the structure shown in the -following example: - -[source,groovy,indent=0,subs="verbatim,quotes"] ----- - beans { - dataSource(BasicDataSource) { - driverClassName = "org.hsqldb.jdbcDriver" - url = "jdbc:hsqldb:mem:grailsDB" - username = "sa" - password = "" - settings = [mynew:"setting"] - } - sessionFactory(SessionFactory) { - dataSource = dataSource - } - myService(MyService) { - nestedBean = { AnotherBean bean -> - dataSource = dataSource - } - } - } ----- - -This configuration style is largely equivalent to XML bean definitions and even -supports Spring's XML configuration namespaces. It also allows for importing XML -bean definition files through an `importBeans` directive. - - [[beans-factory-client]] == Using the Container diff --git a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc index b016731fd495..e4e546684489 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc @@ -1,24 +1,26 @@ [[beans-classpath-scanning]] = Classpath Scanning and Managed Components -Most examples in this chapter use XML to specify the configuration metadata that 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 -metadata through source-level annotations. Even in those examples, however, the "base" -bean definitions are explicitly defined in the XML file, while the annotations drive only -the dependency injection. This section describes an option for implicitly detecting the -candidate components by scanning the classpath. Candidate components are classes that -match against a filter 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 +Most examples in this chapter use XML to specify the configuration metadata that +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 metadata through source-level +annotations. Even in those examples, however, the "base" bean definitions are explicitly +defined in the XML file, while the annotations drive only the dependency injection. + +This section describes an option for implicitly detecting the candidate components by +scanning the classpath. Candidate components are classes that match against filter +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 the container. [NOTE] ==== 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 -use these features. +`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples +of how to use these features. ==== @@ -68,7 +70,7 @@ Java:: // ... } ---- -<1> The `@Component` causes `@Service` to be treated in the same way as `@Component`. +<1> The `@Component` meta-annotation causes `@Service` to be treated in the same way as `@Component`. Kotlin:: + @@ -83,7 +85,7 @@ Kotlin:: // ... } ---- -<1> The `@Component` causes `@Service` to be treated in the same way as `@Component`. +<1> The `@Component` meta-annotation causes `@Service` to be treated in the same way as `@Component`. ====== You can also combine meta-annotations to create "`composed annotations`". For example, @@ -95,7 +97,7 @@ meta-annotations to allow customization. This can be particularly useful when yo want to only expose a subset of the meta-annotation's attributes. For example, Spring's `@SessionScope` annotation hard codes the scope name to `session` but still allows customization of the `proxyMode`. The following listing shows the definition of the -`SessionScope` annotation: +`@SessionScope` annotation: [tabs] ====== @@ -209,7 +211,7 @@ Java:: @Service public class SimpleMovieLister { - private MovieFinder movieFinder; + private final MovieFinder movieFinder; public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; @@ -249,11 +251,11 @@ Kotlin:: ---- ====== - To autodetect these classes and register the corresponding beans, you need to add -`@ComponentScan` to your `@Configuration` class, where the `basePackages` attribute -is a common parent package for the two classes. (Alternatively, you can specify a -comma- or semicolon- or space-separated list that includes the parent package of each class.) +`@ComponentScan` to your `@Configuration` class, where the `basePackages` attribute is +configured with a common parent package for the two classes. Alternatively, you can +specify a comma-, semicolon-, or space-separated list that includes the parent package +of each class. [tabs] ====== @@ -280,10 +282,10 @@ Kotlin:: ---- ====== -NOTE: For brevity, the preceding example could have used the `value` attribute of the -annotation (that is, `@ComponentScan("org.example")`). +TIP: For brevity, the preceding example could have used the implicit `value` attribute of +the annotation instead: `@ComponentScan("org.example")` -The following alternative uses XML: +The following example uses XML configuration: [source,xml,indent=0,subs="verbatim,quotes"] ---- @@ -323,16 +325,68 @@ sure that they are 'opened' (that is, that they use an `opens` declaration inste Furthermore, the `AutowiredAnnotationBeanPostProcessor` and `CommonAnnotationBeanPostProcessor` are both implicitly included when you use the -component-scan element. That means that the two components are autodetected and -wired together -- all without any bean configuration metadata provided in XML. +`` element. That means that the two components are autodetected +and wired together -- all without any bean configuration metadata provided in XML. NOTE: You can disable the registration of `AutowiredAnnotationBeanPostProcessor` and `CommonAnnotationBeanPostProcessor` by including the `annotation-config` attribute with a value of `false`. +[[beans-scanning-placeholders-and-patterns]] +=== Property Placeholders and Ant-style Patterns + +The `basePackages` and `value` attributes in `@ComponentScan` support `${...}` property +placeholders which are resolved against the `Environment` as well as Ant-style package +patterns such as `"org.example.+++**+++"`. + +In addition, multiple packages or patterns may be specified, either separately or within +a single String — for example, `{"org.example.config", "org.example.service.+++**+++"}` +or `"org.example.config, org.example.service.+++**+++"`. + +The following example specifies the `app.scan.packages` property placeholder for the +implicit `value` attribute in `@ComponentScan`. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Configuration +@ComponentScan("${app.scan.packages}") // <1> +public class AppConfig { + // ... +} +---- +<1> `app.scan.packages` property placeholder to be resolved against the `Environment` + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- +@Configuration +@ComponentScan(["\${app.scan.packages}"]) // <1> +class AppConfig { + // ... +} +---- +<1> `app.scan.packages` property placeholder to be resolved against the `Environment` +====== + +The following listing represents a properties file which defines the `app.scan.packages` +property. In the preceding example, it is assumed that this properties file has been +registered with the `Environment` – for example, via `@PropertySource` or a similar +mechanism. + +[source,properties,indent=0,subs="verbatim,quotes"] +---- +app.scan.packages=org.example.config, org.example.service.** +---- + + [[beans-scanning-filters]] -== Using Filters to Customize Scanning +=== Using Filters to Customize Scanning By default, classes annotated with `@Component`, `@Repository`, `@Service`, `@Controller`, `@Configuration`, or a custom annotation that itself is annotated with `@Component` are @@ -369,8 +423,8 @@ The following table describes the filtering options: | A custom implementation of the `org.springframework.core.type.TypeFilter` interface. |=== -The following example shows the configuration ignoring all `@Repository` annotations -and using "`stub`" repositories instead: +The following example shows `@ComponentScan` configuration that excludes all +`@Repository` annotations and includes "`Stub`" repositories instead: [tabs] ====== @@ -422,244 +476,8 @@ annotated or meta-annotated with `@Component`, `@Repository`, `@Service`, `@Cont `@RestController`, or `@Configuration`. -[[beans-factorybeans-annotations]] -== Defining Bean Metadata within Components - -Spring components can also contribute bean definition metadata to the container. You can do -this with the same `@Bean` annotation used to define bean metadata within `@Configuration` -annotated classes. The following example shows how to do so: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Component - public class FactoryMethodComponent { - - @Bean - @Qualifier("public") - public TestBean publicInstance() { - return new TestBean("publicInstance"); - } - - public void doWork() { - // Component method implementation omitted - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - @Component - class FactoryMethodComponent { - - @Bean - @Qualifier("public") - fun publicInstance() = TestBean("publicInstance") - - fun doWork() { - // Component method implementation omitted - } - } ----- -====== - -The preceding class is a Spring component that has application-specific code in its -`doWork()` method. However, it also contributes a bean definition that has a factory -method referring to the method `publicInstance()`. The `@Bean` annotation identifies the -factory method and other bean definition properties, such as a qualifier value through -the `@Qualifier` annotation. Other method-level annotations that can be specified are -`@Scope`, `@Lazy`, and custom qualifier annotations. - -[[beans-factorybeans-annotations-lazy-injection-points]] -[TIP] -==== -In addition to its role for component initialization, you can also place the `@Lazy` -annotation on injection points marked with `@Autowired` or `@Inject`. In this context, -it leads to the injection of a lazy-resolution proxy. However, such a proxy approach -is rather limited. For sophisticated lazy interactions, in particular in combination -with optional dependencies, we recommend `ObjectProvider` instead. -==== - -Autowired fields and methods are supported, as previously discussed, with additional -support for autowiring of `@Bean` methods. The following example shows how to do so: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Component - public class FactoryMethodComponent { - - private static int i; - - @Bean - @Qualifier("public") - public TestBean publicInstance() { - return new TestBean("publicInstance"); - } - - // use of a custom qualifier and autowiring of method parameters - @Bean - protected TestBean protectedInstance( - @Qualifier("public") TestBean spouse, - @Value("#{privateInstance.age}") String country) { - TestBean tb = new TestBean("protectedInstance", 1); - tb.setSpouse(spouse); - tb.setCountry(country); - return tb; - } - - @Bean - private TestBean privateInstance() { - return new TestBean("privateInstance", i++); - } - - @Bean - @RequestScope - public TestBean requestScopedInstance() { - return new TestBean("requestScopedInstance", 3); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - @Component - class FactoryMethodComponent { - - companion object { - private var i: Int = 0 - } - - @Bean - @Qualifier("public") - fun publicInstance() = TestBean("publicInstance") - - // use of a custom qualifier and autowiring of method parameters - @Bean - protected fun protectedInstance( - @Qualifier("public") spouse: TestBean, - @Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply { - this.spouse = spouse - this.country = country - } - - @Bean - private fun privateInstance() = TestBean("privateInstance", i++) - - @Bean - @RequestScope - fun requestScopedInstance() = TestBean("requestScopedInstance", 3) - } ----- -====== - -The example autowires the `String` method parameter `country` to the value of the `age` -property on another bean named `privateInstance`. A Spring Expression Language element -defines the value of the property through the notation `#{ }`. For `@Value` -annotations, an expression resolver is preconfigured to look for bean names when -resolving expression text. - -As of Spring Framework 4.3, you may also declare a factory method parameter of type -`InjectionPoint` (or its more specific subclass: `DependencyDescriptor`) to -access the requesting injection point that triggers the creation of the current bean. -Note that this applies only to the actual creation of bean instances, not to the -injection of existing instances. As a consequence, this feature makes most sense for -beans of prototype scope. For other scopes, the factory method only ever sees the -injection point that triggered the creation of a new bean instance in the given scope -(for example, the dependency that triggered the creation of a lazy singleton bean). -You can use the provided injection point metadata with semantic care in such scenarios. -The following example shows how to use `InjectionPoint`: - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Component - public class FactoryMethodComponent { - - @Bean @Scope("prototype") - public TestBean prototypeInstance(InjectionPoint injectionPoint) { - return new TestBean("prototypeInstance for " + injectionPoint.getMember()); - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - @Component - class FactoryMethodComponent { - - @Bean - @Scope("prototype") - fun prototypeInstance(injectionPoint: InjectionPoint) = - TestBean("prototypeInstance for ${injectionPoint.member}") - } ----- -====== - -The `@Bean` methods in a regular Spring component are processed differently than their -counterparts inside a Spring `@Configuration` class. The difference is that `@Component` -classes are not enhanced with CGLIB to intercept the invocation of methods and fields. -CGLIB proxying is the means by which invoking methods or fields within `@Bean` methods -in `@Configuration` classes creates bean metadata references to collaborating objects. -Such methods are not invoked with normal Java semantics but rather go through the -container in order to provide the usual lifecycle management and proxying of Spring -beans, even when referring to other beans through programmatic calls to `@Bean` methods. -In contrast, invoking a method or field in a `@Bean` method within a plain `@Component` -class has standard Java semantics, with no special CGLIB processing or other -constraints applying. - -[NOTE] -==== -You may declare `@Bean` methods as `static`, allowing for them to be called without -creating their containing configuration class as an instance. This makes particular -sense when defining post-processor beans (for example, of type `BeanFactoryPostProcessor` -or `BeanPostProcessor`), since such beans get initialized early in the container -lifecycle and should avoid triggering other parts of the configuration at that point. - -Calls to static `@Bean` methods never get intercepted by the container, not even within -`@Configuration` classes (as described earlier in this section), due to technical -limitations: CGLIB subclassing can override only non-static methods. As a consequence, -a direct call to another `@Bean` method has standard Java semantics, resulting -in an independent instance being returned straight from the factory method itself. - -The Java language visibility of `@Bean` methods does not have an immediate impact on -the resulting bean definition in Spring's container. You can freely declare your -factory methods as you see fit in non-`@Configuration` classes and also for static -methods anywhere. However, regular `@Bean` methods in `@Configuration` classes need -to be overridable -- that is, they must not be declared as `private` or `final`. - -`@Bean` methods are also discovered on base classes of a given component or -configuration class, as well as on Java 8 default methods declared in interfaces -implemented by the component or configuration class. This allows for a lot of -flexibility in composing complex configuration arrangements, with even multiple -inheritance being possible through Java 8 default methods as of Spring 4.2. - -Finally, a single class may hold multiple `@Bean` methods for the same -bean, as an arrangement of multiple factory methods to use depending on available -dependencies at runtime. This is the same algorithm as for choosing the "`greediest`" -constructor or factory method in other configuration scenarios: The variant with -the largest number of satisfiable dependencies is picked at construction time, -analogous to how the container selects between multiple `@Autowired` constructors. -==== - - [[beans-scanning-name-generator]] -== Naming Autodetected Components +=== Naming Autodetected Components When a component is autodetected as part of the scanning process, its bean name is generated by the `BeanNameGenerator` strategy known to that scanner. @@ -790,7 +608,7 @@ auto-generated names are adequate whenever the container is responsible for wiri [[beans-scanning-scope-resolver]] -== Providing a Scope for Autodetected Components +=== Providing a Scope for Autodetected Components As with Spring-managed components in general, the default and most common scope for autodetected components is `singleton`. However, sometimes you need a different scope @@ -828,10 +646,10 @@ definitions, there is no notion of bean definition inheritance, and inheritance 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, -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, -you may also compose your own scoping annotations by using Spring's meta-annotation -approach: for example, a custom annotation meta-annotated with `@Scope("prototype")`, -possibly also declaring a custom scoped-proxy mode. +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, you may also compose your own scoping +annotations by using Spring's meta-annotation approach: for example, a custom annotation +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 annotation-based approach, you can implement the @@ -873,7 +691,8 @@ Kotlin:: ---- 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 element. The three possible values are: `no`, `interfaces`, and `targetClass`. For example, the following configuration results in standard JDK dynamic proxies: @@ -912,7 +731,7 @@ Kotlin:: [[beans-scanning-qualifiers]] -== Providing Qualifier Metadata with Annotations +=== Providing Qualifier Metadata with Annotations The `@Qualifier` annotation is discussed in xref:core/beans/annotation-config/autowired-qualifiers.adoc[Fine-tuning Annotation-based Autowiring with Qualifiers]. @@ -1002,3 +821,239 @@ NOTE: As with most annotation-based alternatives, keep in mind that the annotati bound to the class definition itself, while the use of XML allows for multiple beans of the same type to provide variations in their qualifier metadata, because that metadata is provided per-instance rather than per-class. + + +[[beans-factorybeans-annotations]] +== Defining Bean Metadata within Components + +Spring components can also contribute bean definition metadata to the container. You can do +this with the same `@Bean` annotation used to define bean metadata within `@Configuration` +annotated classes. The following example shows how to do so: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Component + public class FactoryMethodComponent { + + @Bean + @Qualifier("public") + public TestBean publicInstance() { + return new TestBean("publicInstance"); + } + + public void doWork() { + // Component method implementation omitted + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Component + class FactoryMethodComponent { + + @Bean + @Qualifier("public") + fun publicInstance() = TestBean("publicInstance") + + fun doWork() { + // Component method implementation omitted + } + } +---- +====== + +The preceding class is a Spring component that has application-specific code in its +`doWork()` method. However, it also contributes a bean definition that has a factory +method referring to the method `publicInstance()`. The `@Bean` annotation identifies the +factory method and other bean definition properties, such as a qualifier value through +the `@Qualifier` annotation. Other method-level annotations that can be specified are +`@Scope`, `@Lazy`, and custom qualifier annotations. + +[[beans-factorybeans-annotations-lazy-injection-points]] +[TIP] +==== +In addition to its role for component initialization, you can also place the `@Lazy` +annotation on injection points marked with `@Autowired` or `@Inject`. In this context, +it leads to the injection of a lazy-resolution proxy. However, such a proxy approach +is rather limited. For sophisticated lazy interactions, in particular in combination +with optional dependencies, we recommend `ObjectProvider` instead. +==== + +Autowired fields and methods are supported, as previously discussed, with additional +support for autowiring of `@Bean` methods. The following example shows how to do so: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Component + public class FactoryMethodComponent { + + private static int i; + + @Bean + @Qualifier("public") + public TestBean publicInstance() { + return new TestBean("publicInstance"); + } + + // use of a custom qualifier and autowiring of method parameters + @Bean + protected TestBean protectedInstance( + @Qualifier("public") TestBean spouse, + @Value("#{privateInstance.age}") String country) { + TestBean tb = new TestBean("protectedInstance", 1); + tb.setSpouse(spouse); + tb.setCountry(country); + return tb; + } + + @Bean + private TestBean privateInstance() { + return new TestBean("privateInstance", i++); + } + + @Bean + @RequestScope + public TestBean requestScopedInstance() { + return new TestBean("requestScopedInstance", 3); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Component + class FactoryMethodComponent { + + companion object { + private var i: Int = 0 + } + + @Bean + @Qualifier("public") + fun publicInstance() = TestBean("publicInstance") + + // use of a custom qualifier and autowiring of method parameters + @Bean + protected fun protectedInstance( + @Qualifier("public") spouse: TestBean, + @Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply { + this.spouse = spouse + this.country = country + } + + @Bean + private fun privateInstance() = TestBean("privateInstance", i++) + + @Bean + @RequestScope + fun requestScopedInstance() = TestBean("requestScopedInstance", 3) + } +---- +====== + +The example autowires the `String` method parameter `country` to the value of the `age` +property on another bean named `privateInstance`. A Spring Expression Language element +defines the value of the property through the notation `#{ }`. For `@Value` +annotations, an expression resolver is preconfigured to look for bean names when +resolving expression text. + +As of Spring Framework 4.3, you may also declare a factory method parameter of type +`InjectionPoint` (or its more specific subclass: `DependencyDescriptor`) to +access the requesting injection point that triggers the creation of the current bean. +Note that this applies only to the actual creation of bean instances, not to the +injection of existing instances. As a consequence, this feature makes most sense for +beans of prototype scope. For other scopes, the factory method only ever sees the +injection point that triggered the creation of a new bean instance in the given scope +(for example, the dependency that triggered the creation of a lazy singleton bean). +You can use the provided injection point metadata with semantic care in such scenarios. +The following example shows how to use `InjectionPoint`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Component + public class FactoryMethodComponent { + + @Bean @Scope("prototype") + public TestBean prototypeInstance(InjectionPoint injectionPoint) { + return new TestBean("prototypeInstance for " + injectionPoint.getMember()); + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Component + class FactoryMethodComponent { + + @Bean + @Scope("prototype") + fun prototypeInstance(injectionPoint: InjectionPoint) = + TestBean("prototypeInstance for ${injectionPoint.member}") + } +---- +====== + +The `@Bean` methods in a regular Spring component are processed differently than their +counterparts inside a Spring `@Configuration` class. The difference is that `@Component` +classes are not enhanced with CGLIB to intercept the invocation of methods and fields. +CGLIB proxying is the means by which invoking methods or fields within `@Bean` methods +in `@Configuration` classes creates bean metadata references to collaborating objects. +Such methods are not invoked with normal Java semantics but rather go through the +container in order to provide the usual lifecycle management and proxying of Spring +beans, even when referring to other beans through programmatic calls to `@Bean` methods. +In contrast, invoking a method or field in a `@Bean` method within a plain `@Component` +class has standard Java semantics, with no special CGLIB processing or other +constraints applying. + +[NOTE] +==== +You may declare `@Bean` methods as `static`, allowing for them to be called without +creating their containing configuration class as an instance. This makes particular +sense when defining post-processor beans (for example, of type `BeanFactoryPostProcessor` +or `BeanPostProcessor`), since such beans get initialized early in the container +lifecycle and should avoid triggering other parts of the configuration at that point. + +Calls to static `@Bean` methods never get intercepted by the container, not even within +`@Configuration` classes (as described earlier in this section), due to technical +limitations: CGLIB subclassing can override only non-static methods. As a consequence, +a direct call to another `@Bean` method has standard Java semantics, resulting +in an independent instance being returned straight from the factory method itself. + +The Java language visibility of `@Bean` methods does not have an immediate impact on +the resulting bean definition in Spring's container. You can freely declare your +factory methods as you see fit in non-`@Configuration` classes and also for static +methods anywhere. However, regular `@Bean` methods in `@Configuration` classes need +to be overridable -- that is, they must not be declared as `private` or `final`. + +`@Bean` methods are also discovered on base classes of a given component or +configuration class, as well as on Java default methods declared in interfaces +implemented by the component or configuration class. This allows for a lot of +flexibility in composing complex configuration arrangements, with even multiple +inheritance being possible through Java default methods. + +Finally, a single class may hold multiple `@Bean` methods for the same +bean, as an arrangement of multiple factory methods to use depending on available +dependencies at runtime. This is the same algorithm as for choosing the "`greediest`" +constructor or factory method in other configuration scenarios: The variant with +the largest number of satisfiable dependencies is picked at construction time, +analogous to how the container selects between multiple `@Autowired` constructors. +==== diff --git a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc index 7f6f19572af8..4bafff8d45f3 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/context-introduction.adoc @@ -933,13 +933,12 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- // create a startup step and start recording - StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan"); - // add tagging information to the current step - scanPackages.tag("packages", () -> Arrays.toString(basePackages)); - // perform the actual phase we're instrumenting - this.scanner.scan(basePackages); - // end the current step - scanPackages.end(); + try (StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")) { + // add tagging information to the current step + scanPackages.tag("packages", () -> Arrays.toString(basePackages)); + // perform the actual phase we're instrumenting + this.scanner.scan(basePackages); + } ---- Kotlin:: @@ -947,13 +946,12 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- // create a startup step and start recording - val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan") - // add tagging information to the current step - scanPackages.tag("packages", () -> Arrays.toString(basePackages)) - // perform the actual phase we're instrumenting - this.scanner.scan(basePackages) - // end the current step - scanPackages.end() + try (val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")) { + // add tagging information to the current step + scanPackages.tag("packages", () -> Arrays.toString(basePackages)); + // perform the actual phase we're instrumenting + this.scanner.scan(basePackages); + } ---- ====== diff --git a/framework-docs/modules/ROOT/pages/core/beans/definition.adoc b/framework-docs/modules/ROOT/pages/core/beans/definition.adoc index ed7df447d09f..c010f46148c2 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/definition.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/definition.adoc @@ -99,7 +99,7 @@ the `@Bean` factory method in favor of any pre-declared constructor on the bean **** NOTE: We acknowledge that overriding beans in test scenarios is convenient, and there is -explicit support for this as of Spring Framework 6.2. Please refer to +explicit support for this. Please refer to xref:testing/testcontext-framework/bean-overriding.adoc[this section] for more details. diff --git a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc index 207be618a01a..612451fbcea1 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/dependencies/factory-properties-detailed.adoc @@ -85,11 +85,11 @@ element. The following example shows how to use it: [source,xml,indent=0,subs="verbatim,quotes"] ---- - + - + - + ---- @@ -99,28 +99,24 @@ following snippet: [source,xml,indent=0,subs="verbatim,quotes"] ---- - + - - + + ---- The first form is preferable to the second, because using the `idref` tag lets the -container validate at deployment time that the referenced, named bean actually -exists. In the second variation, no validation is performed on the value that is passed -to the `targetName` property of the `client` bean. Typos are only discovered (with most +container validate at deployment time that the referenced, named bean actually exists. In +the second variation, no validation is performed on the value that is passed to the +`targetName` property of the `client` bean. Typos are therefore only discovered (with most likely fatal results) when the `client` bean is actually instantiated. If the `client` -bean is a xref:core/beans/factory-scopes.adoc[prototype] bean, this typo and the resulting exception -may only be discovered long after the container is deployed. +bean is a xref:core/beans/factory-scopes.adoc[prototype] bean, this typo and the resulting +exception may only be discovered long after the container is deployed. -NOTE: The `local` attribute on the `idref` element is no longer supported in the 4.0 beans -XSD, since it does not provide value over a regular `bean` reference any more. Change -your existing `idref local` references to `idref bean` when upgrading to the 4.0 schema. - -A common place (at least in versions earlier than Spring 2.0) where the `` element -brings value is in the configuration of xref:core/aop-api/pfb.adoc#aop-pfb-1[AOP interceptors] in a -`ProxyFactoryBean` bean definition. Using `` elements when you specify the +NOTE: A common place (at least in versions earlier than Spring 2.0) where the `` +element brings value is in the configuration of xref:core/aop-api/pfb.adoc#aop-pfb-1[AOP interceptors] +in a `ProxyFactoryBean` bean definition. Using `` elements when you specify the interceptor names prevents you from misspelling an interceptor ID. diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc index ea79cbfb22fa..2becb1d7eedd 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc @@ -194,8 +194,7 @@ The following `beans` element uses the `InstantiationTracingBeanPostProcessor`: Notice how the `InstantiationTracingBeanPostProcessor` is merely defined. It does not even have a name, and, because it is a bean, it can be dependency-injected as you would any other bean. (The preceding configuration also defines a bean that is backed by a Groovy -script. The Spring dynamic language support is detailed in the chapter entitled -xref:languages/dynamic.adoc[Dynamic Language Support].) +script.) The following Java application runs the preceding code and configuration: diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc index 6b99094ad0ea..f00c8711a6a4 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/composing-configuration-classes.adoc @@ -327,9 +327,8 @@ Kotlin:: ---- ====== -TIP: Constructor injection in `@Configuration` classes is only supported as of Spring -Framework 4.3. Note also that there is no need to specify `@Autowired` if the target -bean defines only one constructor. +TIP: Note that there is no need to specify `@Autowired` if the target bean defines +only one constructor. [discrete] [[beans-java-injecting-imported-beans-fq]] diff --git a/framework-docs/modules/ROOT/pages/core/beans/java/programmatic-bean-registration.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/programmatic-bean-registration.adoc index f50c94025568..3663fc13fe77 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/java/programmatic-bean-registration.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/java/programmatic-bean-registration.adoc @@ -9,28 +9,7 @@ efficient way. Those bean registrar implementations are typically imported with an `@Import` annotation on `@Configuration` classes. -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @Import(MyBeanRegistrar.class) - class MyConfiguration { - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - @Configuration - @Import(MyBeanRegistrar::class) - class MyConfiguration { - } ----- -====== +include-code::./MyConfiguration[tag=snippet,indent=0] NOTE: You can leverage type-level conditional annotations ({spring-framework-api}/context/annotation/Conditional.html[`@Conditional`], but also other variants) to conditionally import the related bean registrars. @@ -40,49 +19,7 @@ The bean registrar implementation uses {spring-framework-api}/beans/factory/Bean and flexible way. For example, it allows custom registration through an `if` expression, a `for` loop, etc. -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - class MyBeanRegistrar implements BeanRegistrar { - - @Override - public void register(BeanRegistry registry, Environment env) { - registry.registerBean("foo", Foo.class); - registry.registerBean("bar", Bar.class, spec -> spec - .prototype() - .lazyInit() - .description("Custom description") - .supplier(context -> new Bar(context.bean(Foo.class)))); - if (env.matchesProfiles("baz")) { - registry.registerBean(Baz.class, spec -> spec - .supplier(context -> new Baz("Hello World!"))); - } - } - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - class MyBeanRegistrar : BeanRegistrarDsl({ - registerBean() - registerBean( - name = "bar", - prototype = true, - lazyInit = true, - description = "Custom description") { - Bar(bean()) - } - profile("baz") { - registerBean { Baz("Hello World!") } - } - }) ----- -====== +include-code::./MyBeanRegistrar[tag=snippet,indent=0] NOTE: Bean registrars are supported with xref:core/aot.adoc[Ahead of Time Optimizations], either on the JVM or with GraalVM native images, including when instance suppliers are used. diff --git a/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc b/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc index cdd8c6c15a7e..af18ce82e605 100644 --- a/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc +++ b/framework-docs/modules/ROOT/pages/core/databuffer-codec.adoc @@ -3,8 +3,8 @@ 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 -beneficial for performance. For example Netty has the `ByteBuf` hierarchy, Undertow uses -XNIO, Jetty uses pooled byte buffers with a callback to be released, and so on. +beneficial for performance. For example Netty has the `ByteBuf` hierarchy, +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 APIs as follows: diff --git a/framework-docs/modules/ROOT/pages/core/null-safety.adoc b/framework-docs/modules/ROOT/pages/core/null-safety.adoc index 108259e3730f..2460bbe292f4 100644 --- a/framework-docs/modules/ROOT/pages/core/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/core/null-safety.adoc @@ -8,9 +8,9 @@ recommended in order to get familiar with those annotations and semantics. The primary goal of this null-safety arrangement is to prevent a `NullPointerException` from being thrown at runtime via build time checks and to use explicit nullability as a way to express the possible absence of value. -It is useful in both Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting -JSpecify annotations such as IntelliJ IDEA) and Kotlin where JSpecify annotations are automatically translated to -{kotlin-docs}/null-safety.html[Kotlin's null safety]. +It is useful in Java by leveraging nullability checkers such as https://github.com/uber/NullAway[NullAway] or IDEs +supporting JSpecify annotations such as IntelliJ IDEA and Eclipse (the latter requiring manual configuration). In Kotlin, +JSpecify annotations are automatically translated to {kotlin-docs}/null-safety.html[Kotlin's null safety]. The {spring-framework-api}/core/Nullness.html[`Nullness` Spring API] can be used at runtime to detect the nullness of a type usage, a field, a method return type, or a parameter. It provides full support for diff --git a/framework-docs/modules/ROOT/pages/core/resilience.adoc b/framework-docs/modules/ROOT/pages/core/resilience.adoc index b2b4b3f817b5..50432b9fec77 100644 --- a/framework-docs/modules/ROOT/pages/core/resilience.adoc +++ b/framework-docs/modules/ROOT/pages/core/resilience.adoc @@ -1,16 +1,19 @@ [[resilience]] = Resilience Features -As of 7.0, the core Spring Framework includes a couple of common resilience features, -in particular `@Retryable` and `@ConcurrencyLimit` annotations for method invocations. +As of 7.0, the core Spring Framework includes common resilience features, in particular +<> and <> +annotations for method invocations as well as <>. -[[resilience-retryable]] -== Using `@Retryable` +[[resilience-annotations-retryable]] +== `@Retryable` -`@Retryable` is a common annotation that specifies retry characteristics for an individual -method (with the annotation declared at the method level), or for all proxy-invoked -methods in a given class hierarchy (with the annotation declared at the type level). +{spring-framework-api}/resilience/annotation/Retryable.html[`@Retryable`] is an annotation +that specifies retry characteristics for an individual method (with the annotation +declared at the method level), or for all proxy-invoked methods in a given class hierarchy +(with the annotation declared at the type level). [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -20,11 +23,24 @@ public void sendNotification() { } ---- -By default, the method invocation will be retried for any exception thrown: with at -most 3 retry attempts after an initial failure, and a delay of 1 second between attempts. +By default, the method invocation will be retried for any exception thrown: with at most +3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second +between attempts. -This can be specifically adapted for every method if necessary – for example, by narrowing -the exceptions to retry: +[NOTE] +==== +A `@Retryable` method will be invoked at least once and retried at most `maxRetries` +times, where `maxRetries` is the maximum number of retry attempts. Specifically, +`total attempts = 1 initial attempt + maxRetries attempts`. + +For example, if `maxRetries` is set to `4`, the `@Retryable` method will be invoked at +least once and at most 5 times. +==== + +This can be specifically adapted for every method if necessary — for example, by narrowing +the exceptions to retry via the `includes` and `excludes` attributes. The supplied +exception types will be matched against an exception thrown by a failed invocation as well +as nested causes. [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -34,42 +50,63 @@ public void sendNotification() { } ---- -Or for 5 retry attempts and an exponential back-off strategy with a bit of jitter: +NOTE: `@Retryable(MessageDeliveryException.class)` is a shortcut for +`@Retryable(includes{nbsp}={nbsp}MessageDeliveryException.class)`. + +[TIP] +==== +For advanced use cases, you can specify a custom `MethodRetryPredicate` via the +`predicate` attribute in `@Retryable`, and the predicate will be used to determine whether +to retry a failed method invocation based on a `Method` and a given `Throwable` – for +example, by checking the message of the `Throwable`. + +Custom predicates can be combined with `includes` and `excludes`; however, custom +predicates will always be applied after `includes` and `excludes` have been applied. +==== + +Or for 4 retry attempts and an exponential back-off strategy with a bit of jitter: [source,java,indent=0,subs="verbatim,quotes"] ---- -@Retryable(maxAttempts = 5, delay = 100, jitter = 10, multiplier = 2, maxDelay = 1000) +@Retryable( + includes = MessageDeliveryException.class, + maxRetries = 4, + delay = 100, + jitter = 10, + multiplier = 2, + maxDelay = 1000) public void sendNotification() { this.jmsClient.destination("notifications").send(...); } ---- -Last but not least, `@Retryable` also works for reactive methods with a reactive -return type, decorating the pipeline with Reactor's retry capabilities: +Last but not least, `@Retryable` also works for reactive methods with a reactive return +type, decorating the pipeline with Reactor's retry capabilities: [source,java,indent=0,subs="verbatim,quotes"] ---- -@Retryable(maxAttempts = 5, delay = 100, jitter = 10, multiplier = 2, maxDelay = 1000) +@Retryable(maxRetries = 4, delay = 100) public Mono sendNotification() { return Mono.from(...); // <1> } ---- <1> This raw `Mono` will get decorated with a retry spec. -For details on the various characteristics, see the available annotation attributes -in {spring-framework-api}/resilience/annotation/Retryable.html[`@Retryable`]. +For details on the various characteristics, see the available annotation attributes in +{spring-framework-api}/resilience/annotation/Retryable.html[`@Retryable`]. -NOTE: There a `String` variants with placeholder support available for several attributes -as well, as an alternative to the specifically typed annotation attributes used in the -above examples. +TIP: Several attributes in `@Retryable` have `String` variants that provide property +placeholder and SpEL support, as an alternative to the specifically typed annotation +attributes used in the above examples. -[[resilience-concurrency]] -== Using `@ConcurrencyLimit` +[[resilience-annotations-concurrencylimit]] +== `@ConcurrencyLimit` -`@ConcurrencyLimit` is an annotation that specifies a concurrency limit for an individual -method (with the annotation declared at the method level), or for all proxy-invoked -methods in a given class hierarchy (with the annotation declared at the type level). +{spring-framework-api}/resilience/annotation/ConcurrencyLimit.html[`@ConcurrencyLimit`] is +an annotation that specifies a concurrency limit for an individual method (with the +annotation declared at the method level), or for all proxy-invoked methods in a given +class hierarchy (with the annotation declared at the type level). [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -83,33 +120,152 @@ This is meant to protect the target resource from being accessed from too many t the same time, similar to the effect of a pool size limit for a thread pool or a connection pool that blocks access if its limit is reached. -You may optionally set the limit to 1, effectively locking access to the target bean +You may optionally set the limit to `1`, effectively locking access to the target bean instance: [source,java,indent=0,subs="verbatim,quotes"] ---- -@ConcurrencyLimit(1) // <1> +@ConcurrencyLimit(1) public void sendNotification() { this.jmsClient.destination("notifications").send(...); } ---- -<1> 1 is the default, but specifying it makes the intent clearer. -Such limiting is particularly useful with Virtual Threads where there is generally -no thread pool limit in place. For asynchronous tasks, this can be constrained on +Such limiting is particularly useful with Virtual Threads where there is generally no +thread pool limit in place. For asynchronous tasks, this can be constrained on {spring-framework-api}/core/task/SimpleAsyncTaskExecutor.html[`SimpleAsyncTaskExecutor`]. For synchronous invocations, this annotation provides equivalent behavior through {spring-framework-api}/aop/interceptor/ConcurrencyThrottleInterceptor.html[`ConcurrencyThrottleInterceptor`] which has been available since Spring Framework 1.0 for programmatic use with the AOP framework. +TIP: `@ConcurrencyLimit` also has a `limitString` attribute that provides property +placeholder and SpEL support, as an alternative to the `int` based examples above. + + +[[resilience-annotations-configuration]] +== Enabling Resilient Methods + +Like many of Spring's core annotation-based features, `@Retryable` and `@ConcurrencyLimit` +are designed as metadata that you can choose to honor or ignore. The most convenient way +to enable processing of the resilience annotations is to declare +{spring-framework-api}/resilience/annotation/EnableResilientMethods.html[`@EnableResilientMethods`] +on a corresponding `@Configuration` class. + +Alternatively, these annotations can be individually enabled by defining a +`RetryAnnotationBeanPostProcessor` or a `ConcurrencyLimitBeanPostProcessor` bean in the +context. + + +[[resilience-programmatic-retry]] +== Programmatic Retry Support + +In contrast to <> which provides a declarative approach +for specifying retry semantics for methods within beans registered in the +`ApplicationContext`, +{spring-framework-api}/core/retry/RetryTemplate.html[`RetryTemplate`] provides a +programmatic API for retrying arbitrary blocks of code. + +Specifically, a `RetryTemplate` executes and potentially retries a +{spring-framework-api}/core/retry/Retryable.html[`Retryable`] operation based on a +configured {spring-framework-api}/core/retry/RetryPolicy.html[`RetryPolicy`]. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryTemplate = new RetryTemplate(); // <1> + + retryTemplate.execute( + () -> jmsClient.destination("notifications").send(...)); +---- +<1> Implicitly uses `RetryPolicy.withDefaults()`. + +By default, a retryable operation will be retried for any exception thrown: with at most +3 retry attempts (`maxRetries = 3`) after an initial failure, and a delay of 1 second +between attempts. + +If you only need to customize the number of retry attempts, you can use the +`RetryPolicy.withMaxRetries()` factory method as demonstrated below. + +[NOTE] +==== +A retryable operation will be executed at least once and retried at most `maxRetries` +times, where `maxRetries` is the maximum number of retry attempts. Specifically, +`total attempts = 1 initial attempt + maxRetries attempts`. + +For example, if `maxRetries` is set to `4`, the retryable operation will be invoked at +least once and at most 5 times. +==== + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryTemplate = new RetryTemplate(RetryPolicy.withMaxRetries(4)); // <1> + + retryTemplate.execute( + () -> jmsClient.destination("notifications").send(...)); +---- +<1> Explicitly uses `RetryPolicy.withMaxRetries(4)`. + +If you need to narrow the types of exceptions to retry, that can be achieved via the +`includes()` and `excludes()` builder methods. The supplied exception types will be +matched against an exception thrown by a failed operation as well as nested causes. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryPolicy = RetryPolicy.builder() + .includes(MessageDeliveryException.class) // <1> + .excludes(...) // <2> + .build(); + + var retryTemplate = new RetryTemplate(retryPolicy); + + retryTemplate.execute( + () -> jmsClient.destination("notifications").send(...)); +---- +<1> Specify one or more exception types to include. +<2> Specify one or more exception types to exclude. + +[TIP] +==== +For advanced use cases, you can specify a custom `Predicate` via the +`predicate()` method in the `RetryPolicy.Builder`, and the predicate will be used to +determine whether to retry a failed operation based on a given `Throwable` – for example, +by checking the message of the `Throwable`. + +Custom predicates can be combined with `includes` and `excludes`; however, custom +predicates will always be applied after `includes` and `excludes` have been applied. +==== + +The following example demonstrates how to configure a `RetryPolicy` with 4 retry attempts +and an exponential back-off strategy with a bit of jitter. + +[source,java,indent=0,subs="verbatim,quotes"] +---- + var retryPolicy = RetryPolicy.builder() + .includes(MessageDeliveryException.class) + .maxRetries(4) + .delay(Duration.ofMillis(100)) + .jitter(Duration.ofMillis(10)) + .multiplier(2) + .maxDelay(Duration.ofSeconds(1)) + .build(); + + var retryTemplate = new RetryTemplate(retryPolicy); + + retryTemplate.execute( + () -> jmsClient.destination("notifications").send(...)); +---- -[[resilience-enable]] -== Configuring `@EnableResilientMethods` +[TIP] +==== +A {spring-framework-api}/core/retry/RetryListener.html[`RetryListener`] can be registered +with a `RetryTemplate` to react to events published during key retry phases (before a +retry attempt, after a retry attempt, etc.), and you can compose multiple listeners via a +{spring-framework-api}/core/retry/support/CompositeRetryListener.html[`CompositeRetryListener`]. +==== -Note that like many of Spring's core annotation-based features, `@Retryable` and -`@ConcurrencyLimit` are designed as metadata that you can choose to honor or ignore. -The most convenient way to enable actual processing of the resilience annotations -through AOP interception is to declare `@EnableResilientMethods` on a corresponding -configuration class. Alternatively, you may declare `RetryAnnotationBeanPostProcessor` -and/or `ConcurrencyLimitBeanPostProcessor` individually. +Although the factory methods and builder API for `RetryPolicy` cover most common +configuration scenarios, you can implement a custom `RetryPolicy` for complete control +over the types of exceptions that should trigger a retry as well as the +{spring-framework-api}/util/backoff/BackOff.html[`BackOff`] strategy to use. Note that you +can also configure a customized `BackOff` strategy via the `backOff()` method in the +`RetryPolicy.Builder`. diff --git a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc index 9fa68312f598..8dd2442558ae 100644 --- a/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/jdbc/core.adoc @@ -676,7 +676,7 @@ provides `firstName` and `lastName` properties, such as the `Actor` class from a [source,java,indent=0,subs="verbatim,quotes"] ---- this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)") - .paramSource(new Actor("Leonor", "Watling") + .paramSource(new Actor("Leonor", "Watling")) .update(); ---- diff --git a/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc b/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc index 2a5ef2f0eab6..a5c38886a52b 100644 --- a/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/orm/hibernate.adoc @@ -10,11 +10,11 @@ cover the other ORM technologies and show brief examples. [NOTE] ==== -As of Spring Framework 7.0, Spring requires Hibernate ORM 7.0 for Spring's -`HibernateJpaVendorAdapter` as well as for a native Hibernate `SessionFactory` setup. +As of Spring Framework 7.0, Spring requires Hibernate ORM 7.x for Spring's +`HibernateJpaVendorAdapter`. 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. ==== diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc index a16f4985f2d4..42ad16cd0e9e 100644 --- a/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/transaction/declarative/rolling-back.adoc @@ -86,11 +86,13 @@ rollback rules may be configured via the `rollbackFor`/`noRollbackFor` and `rollbackForClassName`/`noRollbackForClassName` attributes, which allow rules to be 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 -against the type of a thrown exception and its super types, providing type safety and -avoiding 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 type – for example, via `rollbackFor` – +that type will be used to match against the type of a thrown exception. Specifically, +given a configured exception type `C`, a thrown exception of type `T` will be considered +a match against `C` if `T` is equal to `C` or a subclass of `C`. This provides type +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 qualified class name or a substring of a fully qualified class name for an exception type diff --git a/framework-docs/modules/ROOT/pages/integration/jms/sending.adoc b/framework-docs/modules/ROOT/pages/integration/jms/sending.adoc index 5ad13d40b189..27beb6225796 100644 --- a/framework-docs/modules/ROOT/pages/integration/jms/sending.adoc +++ b/framework-docs/modules/ROOT/pages/integration/jms/sending.adoc @@ -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 supplied `Session` object: -[source,java,indent=0,subs="verbatim,quotes"] ----- - 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"); - } - }); - } - } ----- +include-code::./JmsQueueSender[] In the preceding example, the `JmsTemplate` is constructed by passing a reference to a `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 `java.util.Map` is converted to a message: -[source,java,indent=0,subs="verbatim,quotes"] ----- - public void sendWithConversion() { - Map 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; - } - }); - } ----- +include-code::./JmsSenderWithConversion[] 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` 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]] == 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` / `MessageProducer` pair, respectively. The `execute()` methods on `JmsTemplate` run 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[] diff --git a/framework-docs/modules/ROOT/pages/integration/observability.adoc b/framework-docs/modules/ROOT/pages/integration/observability.adoc index c9129ef1e544..a32d4b5a2c04 100644 --- a/framework-docs/modules/ROOT/pages/integration/observability.adoc +++ b/framework-docs/modules/ROOT/pages/integration/observability.adoc @@ -189,13 +189,13 @@ This observation uses the `io.micrometer.jakarta9.instrument.jms.DefaultJmsProce [[observability.http-server]] == 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]] === Servlet applications 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. 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. 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: .Low cardinality Keys @@ -228,6 +233,16 @@ By default, the following `KeyValues` are created: |`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]] === Reactive applications diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index 0417613f496e..018ca4e4c73c 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -5,8 +5,8 @@ The Spring Framework provides the following choices for making calls to REST end * xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] -- synchronous client with a fluent API * xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] -- non-blocking, reactive client with fluent API -* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] -- synchronous client with template method API -* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface Clients] -- annotated interface backed by generated proxy +* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] -- synchronous client with template method API, now deprecated in favor of `RestClient` +* xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service Clients] -- annotated interface backed by generated proxy [[rest-restclient]] @@ -479,7 +479,8 @@ See xref:web/webflux-webclient.adoc[WebClient] for more details. The `RestTemplate` provides a high-level API over HTTP client libraries in the form of a classic Spring Template class. It exposes the following groups of overloaded methods: -NOTE: The xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] offers a more modern API for synchronous HTTP access. +WARNING: As of Spring Framework 7.0, `RestTemplate` is deprecated in favor of `RestClient` and will be removed in a future version, +please use the xref:integration/rest-clients.adoc#migrating-to-restclient["Migrating to RestClient"] guide. For asynchronous and streaming scenarios, consider the reactive xref:web/webflux-webclient.adoc[WebClient]. [[rest-overview-of-resttemplate-methods-tbl]] @@ -547,10 +548,26 @@ See the xref:integration/observability.adoc#http-client.resttemplate[RestTemplat Objects passed into and returned from `RestTemplate` methods are converted to and from HTTP messages with the help of an `HttpMessageConverter`, see <>. -=== Migrating from `RestTemplate` to `RestClient` +[[migrating-to-restclient]] +=== Migrating to `RestClient` + +Applications can adopt `RestClient` in a gradual fashion, first focusing on API usage and then on infrastructure setup. +You can consider the following steps: + +1. Create one or more `RestClient` from existing `RestTemplate` instances, like: `RestClient restClient = RestClient.create(restTemplate)`. + Gradually replace `RestTemplate` usage in your application, component by component, by focusing first on issuing requests. + See the table below for API equivalents. +2. Once all client requests go through `RestClient` instances, you can now work on replicating your existing + `RestTemplate` instance creations by using `RestClient.Builder`. Because `RestTemplate` and `RestClient` + share the same infrastructure, you can reuse custom `ClientHttpRequestFactory` or `ClientHttpRequestInterceptor` + in your setup. See xref:integration/rest-clients.adoc#rest-restclient[the `RestClient` builder API]. + +If no other library is available on the classpath, `RestClient` will choose the `JdkClientHttpRequestFactory` +powered by the modern JDK `HttpClient`, whereas `RestTemplate` would pick the `SimpleClientHttpRequestFactory` that +uses `HttpURLConnection`. This can explain subtle behavior difference at runtime at the HTTP level. + The following table shows `RestClient` equivalents for `RestTemplate` methods. -It can be used to migrate from the latter to the former. .RestClient equivalents for RestTemplate methods [cols="1,1", options="header"] @@ -854,9 +871,14 @@ It can be used to migrate from the latter to the former. |=== +`RestClient` and `RestTemplate` instances share the same behavior when it comes to throwing exceptions +(with the `RestClientException` type being at the top of the hierarchy). +When `RestTemplate` consistently throws `HttpClientErrorException` for "4xx" response statues, +`RestClient` allows for more flexibility with custom xref:integration/rest-clients.adoc#rest-http-service-client-exceptions["status handlers"]. -[[rest-http-interface]] -== HTTP Interface Clients + +[[rest-http-service-client]] +== HTTP Service Clients You can define an HTTP Service as a Java interface with `@HttpExchange` methods, and use `HttpServiceProxyFactory` to create a client proxy from it for remote access over HTTP via @@ -928,7 +950,14 @@ Now, you're ready to create client proxies: // Use service methods for remote calls... ---- -[[rest-http-interface-method-parameters]] +HTTP service clients is a powerful and expressive choice for remote access over HTTP. +It allows one team to own the knowledge of how a REST API works, what parts are relevant +to a client application, what input and output types to create, what endpoint method +signatures are needed, what Javadoc to have, and so on. The resulting Java API guides and +is ready to use. + + +[[rest-http-service-client-method-parameters]] === Method Parameters `@HttpExchange` methods support flexible method signatures with the following inputs: @@ -1000,13 +1029,13 @@ parameter annotation) is set to `false`, or the parameter is marked optional as `StreamingHttpOutputMessage.Body` that allows sending the request body by writing to an `OutputStream`. -[[rest-http-interface.custom-resolver]] +[[rest-http-service-client.custom-resolver]] === Custom Arguments You can configure a custom `HttpServiceArgumentResolver`. The example interface below uses a custom `Search` method parameter type: -include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0] +include-code::./CustomHttpServiceArgumentResolver[tag=httpserviceclient,indent=0] A custom argument resolver could be implemented like this: @@ -1021,7 +1050,7 @@ the use of more fine-grained method parameters for individual parts of the reque -[[rest-http-interface-return-values]] +[[rest-http-service-client-return-values]] === Return Values The supported return values depend on the underlying client. @@ -1097,7 +1126,7 @@ underlying HTTP client, which operates at a lower level and provides more contro `InputStream` or `ResponseEntity` that provides access to the raw response body content. -[[rest-http-interface-exceptions]] +[[rest-http-service-client-exceptions]] === Error Handling To customize error handling for HTTP Service client proxies, you can configure the @@ -1134,7 +1163,7 @@ documentation for each client, as well as the Javadoc of `defaultStatusHandler` -[[rest-http-interface-adapter-decorator]] +[[rest-http-service-client-adapter-decorator]] === Decorating the Adapter `HttpExchangeAdapter` and `ReactorHttpExchangeAdapter` are contracts that decouple HTTP @@ -1162,8 +1191,8 @@ built-in decorators to suppress 404 exceptions and return a `ResponseEntity` wit -[[rest-http-interface-group-config]] -=== HTTP Interface Groups +[[rest-http-service-client-group-config]] +=== HTTP Service Groups It's trivial to create client proxies with `HttpServiceProxyFactory`, but to have them declared as beans leads to repetitive configuration. You may also have multiple diff --git a/framework-docs/modules/ROOT/pages/languages/dynamic.adoc b/framework-docs/modules/ROOT/pages/languages/dynamic.adoc deleted file mode 100644 index 7afe1d3d0558..000000000000 --- a/framework-docs/modules/ROOT/pages/languages/dynamic.adoc +++ /dev/null @@ -1,831 +0,0 @@ -[[dynamic-language]] -= Dynamic Language Support - -Spring provides comprehensive support for using classes and objects that have been -defined by using a dynamic language (such as Groovy) with Spring. This support lets -you write any number of classes in a supported dynamic language and have the Spring -container transparently instantiate, configure, and dependency inject the resulting -objects. - -Spring's scripting support primarily targets Groovy and BeanShell. Beyond those -specifically supported languages, the JSR-223 scripting mechanism is supported -for integration with any JSR-223 capable language provider (as of Spring 4.2), -for example, JRuby. - -You can find fully working examples of where this dynamic language support can be -immediately useful in xref:languages/dynamic.adoc#dynamic-language-scenarios[Scenarios]. - - -[[dynamic-language-a-first-example]] -== A First Example - -The bulk of this chapter is concerned with describing the dynamic language support in -detail. Before diving into all of the ins and outs of the dynamic language support, -we look at a quick example of a bean defined in a dynamic language. The dynamic -language for this first bean is Groovy. (The basis of this example was taken from the -Spring test suite. If you want to see equivalent examples in any of the other -supported languages, take a look at the source code). - -The next example shows the `Messenger` interface, which the Groovy bean is going to -implement. Note that this interface is defined in plain Java. Dependent objects that -are injected with a reference to the `Messenger` do not know that the underlying -implementation is a Groovy script. The following listing shows the `Messenger` interface: - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - public interface Messenger { - - String getMessage(); - } ----- - -The following example defines a class that has a dependency on the `Messenger` interface: - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - public class DefaultBookingService implements BookingService { - - private Messenger messenger; - - public void setMessenger(Messenger messenger) { - this.messenger = messenger; - } - - public void processBooking() { - // use the injected Messenger object... - } - } ----- - -The following example implements the `Messenger` interface in Groovy: - -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages",fold="none"] ----- - package org.springframework.scripting.groovy - - // Import the Messenger interface (written in Java) that is to be implemented - import org.springframework.scripting.Messenger - - // Define the implementation in Groovy in file 'Messenger.groovy' - class GroovyMessenger implements Messenger { - - String message - } ----- - -[NOTE] -==== -To use the custom dynamic language tags to define dynamic-language-backed beans, you -need to have the XML Schema preamble at the top of your Spring XML configuration file. -You also need to use a Spring `ApplicationContext` implementation as your IoC -container. Using the dynamic-language-backed beans with a plain `BeanFactory` -implementation is supported, but you have to manage the plumbing of the Spring internals -to do so. - -For more information on schema-based configuration, see xref:languages/dynamic.adoc#xsd-schemas-lang[XML Schema-based Configuration] -. -==== - -Finally, the following example shows the bean definitions that effect the injection of the -Groovy-defined `Messenger` implementation into an instance of the -`DefaultBookingService` class: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - - - ----- - -The `bookingService` bean (a `DefaultBookingService`) can now use its private `messenger` -member variable as normal, because the `Messenger` instance that was injected into it is -a `Messenger` instance. There is nothing special going on here -- just plain Java and -plain Groovy. - -Hopefully, the preceding XML snippet is self-explanatory, but do not worry unduly if it is not. -Keep reading for the in-depth detail on the whys and wherefores of the preceding configuration. - - -[[dynamic-language-beans]] -== Defining Beans that Are Backed by Dynamic Languages - -This section describes exactly how you define Spring-managed beans in any of the -supported dynamic languages. - -Note that this chapter does not attempt to explain the syntax and idioms of the supported -dynamic languages. For example, if you want to use Groovy to write certain of the classes -in your application, we assume that you already know Groovy. If you need further details -about the dynamic languages themselves, see -xref:languages/dynamic.adoc#dynamic-language-resources[Further Resources] at the end of -this chapter. - -[[dynamic-language-beans-concepts]] -=== Common Concepts - -The steps involved in using dynamic-language-backed beans are as follows: - -. Write the test for the dynamic language source code (naturally). -. Then write the dynamic language source code itself. -. Define your dynamic-language-backed beans by using the appropriate `` - element in the XML configuration (you can define such beans programmatically by - using the Spring API, although you will have to consult the source code for - directions on how to do this, as this chapter does not cover this type of advanced configuration). - Note that this is an iterative step. You need at least one bean definition for each dynamic - language source file (although multiple bean definitions can reference the same source file). - -The first two steps (testing and writing your dynamic language source files) are beyond -the scope of this chapter. See the language specification and reference manual -for your chosen dynamic language and crack on with developing your dynamic language -source files. You first want to read the rest of this chapter, though, as -Spring's dynamic language support does make some (small) assumptions about the contents -of your dynamic language source files. - -[[dynamic-language-beans-concepts-xml-language-element]] -==== The element - -The final step in the list in the xref:languages/dynamic.adoc#dynamic-language-beans-concepts[preceding section] -involves defining dynamic-language-backed bean definitions, one for each bean that you -want to configure (this is no different from normal JavaBean configuration). However, -instead of specifying the fully qualified class name of the class that is to be -instantiated and configured by the container, you can use the `` -element to define the dynamic language-backed bean. - -Each of the supported languages has a corresponding `` element: - -* `` (Groovy) -* `` (BeanShell) -* `` (JSR-223, for example, with JRuby) - -The exact attributes and child elements that are available for configuration depends on -exactly which language the bean has been defined in (the language-specific sections -later in this chapter detail this). - -[[dynamic-language-refreshable-beans]] -==== Refreshable Beans - -One of the (and perhaps the single) most compelling value adds of the dynamic language -support in Spring is the "`refreshable bean`" feature. - -A refreshable bean is a dynamic-language-backed bean. With a small amount of -configuration, a dynamic-language-backed bean can monitor changes in its underlying -source file resource and then reload itself when the dynamic language source file is -changed (for example, when you edit and save changes to the file on the file system). - -This lets you deploy any number of dynamic language source files as part of an -application, configure the Spring container to create beans backed by dynamic -language source files (using the mechanisms described in this chapter), and (later, -as requirements change or some other external factor comes into play) edit a dynamic -language source file and have any change they make be reflected in the bean that is -backed by the changed dynamic language source file. There is no need to shut down a -running application (or redeploy in the case of a web application). The -dynamic-language-backed bean so amended picks up the new state and logic from the -changed dynamic language source file. - -NOTE: This feature is off by default. - -Now we can take a look at an example to see how easy it is to start using refreshable -beans. To turn on the refreshable beans feature, you have to specify exactly one -additional attribute on the `` element of your bean definition. So, -if we stick with xref:languages/dynamic.adoc#dynamic-language-a-first-example[the example] from earlier in -this chapter, the following example shows what we would change in the Spring XML -configuration to effect refreshable beans: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - script-source="classpath:Messenger.groovy"> - - - - - - - - ----- - -That really is all you have to do. The `refresh-check-delay` attribute defined on the -`messenger` bean definition is the number of milliseconds after which the bean is -refreshed with any changes made to the underlying dynamic language source file. -You can turn off the refresh behavior by assigning a negative value to the -`refresh-check-delay` attribute. Remember that, by default, the refresh behavior is -disabled. If you do not want the refresh behavior, do not define the attribute. - -If we then run the following application, we can exercise the refreshable feature. -(Please excuse the "`jumping-through-hoops-to-pause-the-execution`" shenanigans -in this next slice of code.) The `System.in.read()` call is only there so that the -execution of the program pauses while you (the developer in this scenario) go off -and edit the underlying dynamic language source file so that the refresh triggers -on the dynamic-language-backed bean when the program resumes execution. - -The following listing shows this sample application: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.context.ApplicationContext; - import org.springframework.context.support.ClassPathXmlApplicationContext; - import org.springframework.scripting.Messenger; - - public final class Boot { - - public static void main(final String[] args) throws Exception { - ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); - Messenger messenger = (Messenger) ctx.getBean("messenger"); - System.out.println(messenger.getMessage()); - // pause execution while I go off and make changes to the source file... - System.in.read(); - System.out.println(messenger.getMessage()); - } - } ----- - -Assume then, for the purposes of this example, that all calls to the `getMessage()` -method of `Messenger` implementations have to be changed such that the message is -surrounded by quotation marks. The following listing shows the changes that you -(the developer) should make to the `Messenger.groovy` source file when the -execution of the program is paused: - -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting - - class GroovyMessenger implements Messenger { - - private String message = "Bingo" - - public String getMessage() { - // change the implementation to surround the message in quotes - return "'" + this.message + "'" - } - - public void setMessage(String message) { - this.message = message - } - } ----- - -When the program runs, the output before the input pause will be `I Can Do The Frug`. -After the change to the source file is made and saved and the program resumes execution, -the result of calling the `getMessage()` method on the dynamic-language-backed -`Messenger` implementation is `'I Can Do The Frug'` (notice the inclusion of the -additional quotation marks). - -Changes to a script do not trigger a refresh if the changes occur within the window of -the `refresh-check-delay` value. Changes to the script are not actually picked up until -a method is called on the dynamic-language-backed bean. It is only when a method is -called on a dynamic-language-backed bean that it checks to see if its underlying script -source has changed. Any exceptions that relate to refreshing the script (such as -encountering a compilation error or finding that the script file has been deleted) -results in a fatal exception being propagated to the calling code. - -The refreshable bean behavior described earlier does not apply to dynamic language -source files defined with the `` element notation (see -xref:languages/dynamic.adoc#dynamic-language-beans-inline[Inline Dynamic Language Source Files]). -Additionally, it applies only to beans where changes to the underlying source file can -actually be detected (for example, by code that checks the last modified date of a -dynamic language source file that exists on the file system). - -[[dynamic-language-beans-inline]] -==== Inline Dynamic Language Source Files - -The dynamic language support can also cater to dynamic language source files that are -embedded directly in Spring bean definitions. More specifically, the -`` element lets you define dynamic language source immediately -inside a Spring configuration file. An example might clarify how the inline script -feature works: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - package org.springframework.scripting.groovy - - import org.springframework.scripting.Messenger - - class GroovyMessenger implements Messenger { - String message - } - - - - ----- - -If we put to one side the issues surrounding whether it is good practice to define -dynamic language source inside a Spring configuration file, the `` -element can be useful in some scenarios. For instance, we might want to quickly add a -Spring `Validator` implementation to a Spring MVC `Controller`. This is but a moment's -work using inline source. (See -xref:languages/dynamic.adoc#dynamic-language-scenarios-validators[Scripted Validators] -for such an example.) - -[[dynamic-language-beans-ctor-injection]] -==== Understanding Constructor Injection in the Context of Dynamic-language-backed Beans - -There is one very important thing to be aware of with regard to Spring's dynamic -language support. Namely, you can not (currently) supply constructor arguments -to dynamic-language-backed beans (and, hence, constructor-injection is not available for -dynamic-language-backed beans). In the interests of making this special handling of -constructors and properties 100% clear, the following mixture of code and configuration -does not work: - -.An approach that cannot work -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting.groovy - - import org.springframework.scripting.Messenger - - // from the file 'Messenger.groovy' - class GroovyMessenger implements Messenger { - - GroovyMessenger() {} - - // this constructor is not available for Constructor Injection - GroovyMessenger(String message) { - this.message = message; - } - - String message - - String anotherMessage - } ----- - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- - -In practice this limitation is not as significant as it first appears, since setter -injection is the injection style favored by the overwhelming majority of developers -(we leave the discussion as to whether that is a good thing to another day). - -[[dynamic-language-beans-groovy]] -=== Groovy Beans - -This section describes how to use beans defined in Groovy in Spring. - -The Groovy homepage includes the following description: - -"`Groovy is an agile dynamic language for the Java 2 Platform that has many of the -features that people like so much in languages like Python, Ruby and Smalltalk, making -them available to Java developers using a Java-like syntax.`" - -If you have read this chapter straight from the top, you have already -xref:languages/dynamic.adoc#dynamic-language-a-first-example[seen an example] of a Groovy-dynamic-language-backed -bean. Now consider another example (again using an example from the Spring test suite): - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - public interface Calculator { - - int add(int x, int y); - } ----- - -The following example implements the `Calculator` interface in Groovy: - -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting.groovy - - // from the file 'calculator.groovy' - class GroovyCalculator implements Calculator { - - int add(int x, int y) { - x + y - } - } ----- - -The following bean definition uses the calculator defined in Groovy: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - ----- - -Finally, the following small application exercises the preceding configuration: - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - import org.springframework.context.ApplicationContext; - import org.springframework.context.support.ClassPathXmlApplicationContext; - - public class Main { - - public static void main(String[] args) { - ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); - Calculator calc = ctx.getBean("calculator", Calculator.class); - System.out.println(calc.add(2, 8)); - } - } ----- - -The resulting output from running the above program is (unsurprisingly) `10`. -(For more interesting examples, see the dynamic language showcase project for a more -complex example or see the examples xref:languages/dynamic.adoc#dynamic-language-scenarios[Scenarios] later in this chapter). - -You must not define more than one class per Groovy source file. While this is perfectly -legal in Groovy, it is (arguably) a bad practice. In the interests of a consistent -approach, you should (in the opinion of the Spring team) respect the standard Java -conventions of one (public) class per source file. - -[[dynamic-language-beans-groovy-customizer]] -==== Customizing Groovy Objects by Using a Callback - -The `GroovyObjectCustomizer` interface is a callback that lets you hook additional -creation logic into the process of creating a Groovy-backed bean. For example, -implementations of this interface could invoke any required initialization methods, -set some default property values, or specify a custom `MetaClass`. The following listing -shows the `GroovyObjectCustomizer` interface definition: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - public interface GroovyObjectCustomizer { - - void customize(GroovyObject goo); - } ----- - -The Spring Framework instantiates an instance of your Groovy-backed bean and then -passes the created `GroovyObject` to the specified `GroovyObjectCustomizer` (if one -has been defined). You can do whatever you like with the supplied `GroovyObject` -reference. We expect that most people want to set a custom `MetaClass` with this -callback, and the following example shows how to do so: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - public final class SimpleMethodTracingCustomizer implements GroovyObjectCustomizer { - - public void customize(GroovyObject goo) { - DelegatingMetaClass metaClass = new DelegatingMetaClass(goo.getMetaClass()) { - - public Object invokeMethod(Object object, String methodName, Object[] arguments) { - System.out.println("Invoking '" + methodName + "'."); - return super.invokeMethod(object, methodName, arguments); - } - }; - metaClass.initialize(); - goo.setMetaClass(metaClass); - } - - } ----- - -A full discussion of meta-programming in Groovy is beyond the scope of the Spring -reference manual. See the relevant section of the Groovy reference manual or do a -search online. Plenty of articles address this topic. Actually, making use of a -`GroovyObjectCustomizer` is easy if you use the Spring namespace support, as the -following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - ----- - -If you do not use the Spring namespace support, you can still use the -`GroovyObjectCustomizer` functionality, as the following example shows: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - ----- - -NOTE: You may also specify a Groovy `CompilationCustomizer` (such as an `ImportCustomizer`) -or even a full Groovy `CompilerConfiguration` object in the same place as Spring's -`GroovyObjectCustomizer`. Furthermore, you may set a common `GroovyClassLoader` with custom -configuration for your beans at the `ConfigurableApplicationContext.setClassLoader` level; -this also leads to shared `GroovyClassLoader` usage and is therefore recommendable in case of -a large number of scripted beans (avoiding an isolated `GroovyClassLoader` instance per bean). - -[[dynamic-language-beans-bsh]] -=== BeanShell Beans - -This section describes how to use BeanShell beans in Spring. - -The https://beanshell.github.io/intro.html[BeanShell homepage] includes the following -description: - ----- -BeanShell is a small, free, embeddable Java source interpreter with dynamic language -features, written in Java. BeanShell dynamically runs standard Java syntax and -extends it with common scripting conveniences such as loose types, commands, and method -closures like those in Perl and JavaScript. ----- - -In contrast to Groovy, BeanShell-backed bean definitions require some (small) additional -configuration. The implementation of the BeanShell dynamic language support in Spring is -interesting, because Spring creates a JDK dynamic proxy that implements all of the -interfaces that are specified in the `script-interfaces` attribute value of the -`` element (this is why you must supply at least one interface in the value -of the attribute, and, consequently, program to interfaces when you use BeanShell-backed -beans). This means that every method call on a BeanShell-backed object goes through the -JDK dynamic proxy invocation mechanism. - -Now we can show a fully working example of using a BeanShell-based bean that implements -the `Messenger` interface that was defined earlier in this chapter. We again show the -definition of the `Messenger` interface: - -[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.scripting; - - public interface Messenger { - - String getMessage(); - } ----- - -The following example shows the BeanShell "`implementation`" (we use the term loosely here) -of the `Messenger` interface: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - String message; - - String getMessage() { - return message; - } - - void setMessage(String aMessage) { - message = aMessage; - } ----- - -The following example shows the Spring XML that defines an "`instance`" of the above -"`class`" (again, we use these terms very loosely here): - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - ----- - -See xref:languages/dynamic.adoc#dynamic-language-scenarios[Scenarios] for some scenarios -where you might want to use BeanShell-based beans. - - -[[dynamic-language-scenarios]] -== Scenarios - -The possible scenarios where defining Spring managed beans in a scripting language would -be beneficial are many and varied. This section describes two possible use cases for the -dynamic language support in Spring. - -[[dynamic-language-scenarios-controllers]] -=== Scripted Spring MVC Controllers - -One group of classes that can benefit from using dynamic-language-backed beans is that -of Spring MVC controllers. In pure Spring MVC applications, the navigational flow -through a web application is, to a large extent, determined by code encapsulated within -your Spring MVC controllers. As the navigational flow and other presentation layer logic -of a web application needs to be updated to respond to support issues or changing -business requirements, it may well be easier to effect any such required changes by -editing one or more dynamic language source files and seeing those changes being -immediately reflected in the state of a running application. - -Remember that, in the lightweight architectural model espoused by projects such as -Spring, you typically aim to have a really thin presentation layer, with all -the meaty business logic of an application being contained in the domain and service -layer classes. Developing Spring MVC controllers as dynamic-language-backed beans lets -you change presentation layer logic by editing and saving text files. Any -changes to such dynamic language source files is (depending on the configuration) -automatically reflected in the beans that are backed by dynamic language source files. - -NOTE: To effect this automatic "`pickup`" of any changes to dynamic-language-backed -beans, you have to enable the "`refreshable beans`" functionality. See -xref:languages/dynamic.adoc#dynamic-language-refreshable-beans[Refreshable Beans] for a full treatment of this feature. - -The following example shows an `org.springframework.web.servlet.mvc.Controller` implemented -by using the Groovy dynamic language: - -[source,groovy,indent=0,subs="verbatim,quotes",chomp="-packages"] ----- - package org.springframework.showcase.fortune.web - - import org.springframework.showcase.fortune.service.FortuneService - import org.springframework.showcase.fortune.domain.Fortune - import org.springframework.web.servlet.ModelAndView - import org.springframework.web.servlet.mvc.Controller - - import jakarta.servlet.http.HttpServletRequest - import jakarta.servlet.http.HttpServletResponse - - // from the file '/WEB-INF/groovy/FortuneController.groovy' - class FortuneController implements Controller { - - @Property FortuneService fortuneService - - ModelAndView handleRequest(HttpServletRequest request, - HttpServletResponse httpServletResponse) { - return new ModelAndView("tell", "fortune", this.fortuneService.tellFortune()) - } - } ----- - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - ----- - -[[dynamic-language-scenarios-validators]] -=== Scripted Validators - -Another area of application development with Spring that may benefit from the -flexibility afforded by dynamic-language-backed beans is that of validation. It can -be easier to express complex validation logic by using a loosely typed dynamic language -(that may also have support for inline regular expressions) as opposed to regular Java. - -Again, developing validators as dynamic-language-backed beans lets you change -validation logic by editing and saving a simple text file. Any such changes is -(depending on the configuration) automatically reflected in the execution of a -running application and would not require the restart of an application. - -NOTE: To effect the automatic "`pickup`" of any changes to dynamic-language-backed -beans, you have to enable the 'refreshable beans' feature. See -xref:languages/dynamic.adoc#dynamic-language-refreshable-beans[Refreshable Beans] -for a full and detailed treatment of this feature. - -The following example shows a Spring `org.springframework.validation.Validator` -implemented by using the Groovy dynamic language (see -xref:core/validation/validator.adoc[Validation using Spring’s Validator interface] -for a discussion of the `Validator` interface): - -[source,groovy,indent=0,subs="verbatim,quotes"] ----- - import org.springframework.validation.Validator - import org.springframework.validation.Errors - import org.springframework.beans.TestBean - - class TestBeanValidator implements Validator { - - boolean supports(Class clazz) { - return TestBean.class.isAssignableFrom(clazz) - } - - void validate(Object bean, Errors errors) { - if(bean.name?.trim()?.size() > 0) { - return - } - errors.reject("whitespace", "Cannot be composed wholly of whitespace.") - } - } ----- - - -[[dynamic-language-final-notes]] -== Additional Details - -This last section contains some additional details related to the dynamic language support. - -[[dynamic-language-final-notes-aop]] -=== AOP -- Advising Scripted Beans - -You can use the Spring AOP framework to advise scripted beans. The Spring AOP -framework actually is unaware that a bean that is being advised might be a scripted -bean, so all of the AOP use cases and functionality that you use (or aim to use) -work with scripted beans. When you advise scripted beans, you cannot use class-based -proxies. You must use xref:core/aop/proxying.adoc[interface-based proxies]. - -You are not limited to advising scripted beans. You can also write aspects themselves -in a supported dynamic language and use such beans to advise other Spring beans. -This really would be an advanced use of the dynamic language support though. - -[[dynamic-language-final-notes-scopes]] -=== Scoping - -In case it is not immediately obvious, scripted beans can be scoped in the same way as -any other bean. The `scope` attribute on the various `` elements lets -you control the scope of the underlying scripted bean, as it does with a regular -bean. (The default scope is xref:core/beans/factory-scopes.adoc#beans-factory-scopes-singleton[singleton], -as it is with "`regular`" beans.) - -The following example uses the `scope` attribute to define a Groovy bean scoped as -a xref:core/beans/factory-scopes.adoc#beans-factory-scopes-prototype[prototype]: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - - - - - - - ----- - -See xref:core/beans/factory-scopes.adoc[Bean Scopes] in -xref:web/webmvc-view/mvc-xslt.adoc#mvc-view-xslt-beandefs[The IoC Container] -for a full discussion of the scoping support in the Spring Framework. - -[[xsd-schemas-lang]] -=== The `lang` XML schema - -The `lang` elements in Spring XML configuration deal with exposing objects that have been -written in a dynamic language (such as Groovy or BeanShell) as beans in the Spring container. - -These elements (and the dynamic language support) are comprehensively covered in -xref:languages/dynamic.adoc[Dynamic Language Support]. See that section -for full details on this support and the `lang` elements. - -To use the elements in the `lang` schema, you need to have the following preamble at the -top of your Spring XML configuration file. The text in the following snippet references -the correct schema so that the tags in the `lang` namespace are available to you: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - - - - - - ----- - - -[[dynamic-language-resources]] -== Further Resources - -The following links go to further resources about the various dynamic languages referenced -in this chapter: - -* The https://www.groovy-lang.org/[Groovy] homepage -* The https://beanshell.github.io/intro.html[BeanShell] homepage -* The https://www.jruby.org[JRuby] homepage diff --git a/framework-docs/modules/ROOT/pages/languages/groovy.adoc b/framework-docs/modules/ROOT/pages/languages/groovy.adoc index e50f136b23bf..a552fdf61eab 100644 --- a/framework-docs/modules/ROOT/pages/languages/groovy.adoc +++ b/framework-docs/modules/ROOT/pages/languages/groovy.adoc @@ -6,9 +6,37 @@ Groovy is a powerful, optionally typed, and dynamic language, with static-typing compilation capabilities. It offers a concise syntax and integrates smoothly with any existing Java application. +[[beans-factory-groovy]] +== The Groovy Bean Definition DSL + The Spring Framework provides a dedicated `ApplicationContext` that supports a Groovy-based -Bean Definition DSL. For more details, see -xref:core/beans/basics.adoc#beans-factory-groovy[The Groovy Bean Definition DSL]. +Bean Definition DSL, as known from the Grails framework. + +Typically, such configuration live in a ".groovy" file with the structure shown in the +following example: + +[source,groovy,indent=0,subs="verbatim,quotes"] +---- + beans { + dataSource(BasicDataSource) { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:mem:grailsDB" + username = "sa" + password = "" + settings = [mynew:"setting"] + } + sessionFactory(SessionFactory) { + dataSource = dataSource + } + myService(MyService) { + nestedBean = { AnotherBean bean -> + dataSource = dataSource + } + } + } +---- + +This configuration style is largely equivalent to XML bean definitions and even +supports Spring's XML configuration namespaces. It also allows for importing XML +bean definition files through an `importBeans` directive. -Further support for Groovy, including beans written in Groovy, refreshable script beans, -and more is available in xref:languages/dynamic.adoc[Dynamic Language Support]. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/bean-registration-dsl.adoc similarity index 62% rename from framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc rename to framework-docs/modules/ROOT/pages/languages/kotlin/bean-registration-dsl.adoc index db7e1809ae95..da759af2bf0c 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/bean-registration-dsl.adoc @@ -1,4 +1,4 @@ -[[kotlin-bean-definition-dsl]] -= Bean Definition DSL +[[kotlin-bean-registration-dsl]] += Bean Registration DSL See xref:core/beans/java/programmatic-bean-registration.adoc[Programmatic Bean Registration]. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc index 7d8dd95ae73f..17729c6fda29 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc @@ -250,3 +250,29 @@ For Kotlin `Flow`, a `Flow.transactional` extension is provided. } } ---- + +[[coroutines.propagation]] +== Context Propagation + +Spring applications are xref:integration/observability.adoc[instrumented with Micrometer for Observability support]. +For tracing support, the current observation is propagated through a `ThreadLocal` for blocking code, +or the Reactor `Context` for reactive pipelines. But the current observation also needs to be made available +in the execution context of a suspended function. Without that, the current "traceId" will not be automatically +prepended to logged statements from coroutines. + +The {spring-framework-api-kdoc}/spring-core/org.springframework.core/-propagation-context-element/index.html[`PropagationContextElement`] operator generally ensures that the +{micrometer-context-propagation-docs}/[Micrometer Context Propagation library] works with Kotlin Coroutines. + +It requires the `io.micrometer:context-propagation` dependency and optionally the +`org.jetbrains.kotlinx:kotlinx-coroutines-reactor` one. Automatic context propagation via +`CoroutinesUtils#invokeSuspendingFunction` (used by Spring to adapt Coroutines to Reactor `Flux` or `Mono`) can be +enabled by invoking `Hooks.enableAutomaticContextPropagation()`. + +Applications can also use `PropagationContextElement` explicitly to augment the `CoroutineContext` +with the context propagation mechanism: + +include-code::./ContextPropagationSample[tag=context,indent=0] + +Here, assuming that Micrometer Tracing is configured, the resulting logging statement will show the current "traceId" +and unlock better observability for your application. + diff --git a/framework-docs/modules/ROOT/pages/overview.adoc b/framework-docs/modules/ROOT/pages/overview.adoc index e7ff8af9a18b..8ac7c152c6b8 100644 --- a/framework-docs/modules/ROOT/pages/overview.adoc +++ b/framework-docs/modules/ROOT/pages/overview.adoc @@ -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, 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, -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 early days of J2EE and Spring, applications were created to be deployed to an application diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc index 4cc25b1c1765..2b072e8b0e5a 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-mockitobean.adoc @@ -77,14 +77,35 @@ exactly one candidate bean exists. [TIP] ==== -Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean -will result in an exception. +As stated in the documentation for Mockito, there are times when using `Mockito.when()` is +inappropriate for stubbing a spy – for example, if calling a real method on a spy results +in undesired side effects. + +To avoid such undesired side effects, consider using +`Mockito.doReturn(...).when(spy)...`, `Mockito.doThrow(...).when(spy)...`, +`Mockito.doNothing().when(spy)...`, and similar methods. +==== + +[NOTE] +==== +When using `@MockitoBean` to mock a non-singleton bean, the non-singleton bean will be +replaced with a singleton mock, and the corresponding bean definition will be converted +to a `singleton`. Consequently, if you mock a `prototype` or scoped bean, the mock will +be treated as a `singleton`. + +Similarly, when using `@MockitoSpyBean` to create a spy for a non-singleton bean, the +corresponding bean definition will be converted to a `singleton`. Consequently, if you +create a spy for a `prototype` or scoped bean, the spy will be treated as a `singleton`. When using `@MockitoBean` to mock a bean created by a `FactoryBean`, the `FactoryBean` will be replaced with a singleton mock of the type of object created by the `FactoryBean`. -When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be created -for the object created by the `FactoryBean`, not for the `FactoryBean` itself. +Similarly, when using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will +be created for the object created by the `FactoryBean`, not for the `FactoryBean` itself. + +Furthermore, `@MockitoSpyBean` cannot be used to spy on a scoped proxy — for example, a +bean annotated with `@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)`. Any attempt to do +so will fail with an exception. ==== [NOTE] diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc index 4ec33c0c154f..b752becaaa96 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-spring/annotation-testbean.adoc @@ -116,12 +116,15 @@ fully-qualified method name following the syntax `#< – for example, `methodName = "org.example.TestUtils#createCustomService"`. ==== -[TIP] +[NOTE] ==== -Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean -will result in an exception. - -When overriding a bean created by a `FactoryBean`, the `FactoryBean` will be replaced -with a singleton bean corresponding to the value returned from the `@TestBean` factory -method. +When overriding a non-singleton bean, the non-singleton bean will be replaced with a +singleton bean corresponding to the value returned from the `@TestBean` factory method, +and the corresponding bean definition will be converted to a `singleton`. Consequently, +if `@TestBean` is used to override a `prototype` or scoped bean, the overridden bean will +be treated as a `singleton`. + +Similarly, when overriding a bean created by a `FactoryBean`, the `FactoryBean` will be +replaced with a singleton bean corresponding to the value returned from the `@TestBean` +factory method. ==== diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc index d3a8df109d38..a9e3533cac50 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc @@ -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] 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 `CreateMessagePage` implementation: diff --git a/framework-docs/modules/ROOT/pages/testing/resttestclient.adoc b/framework-docs/modules/ROOT/pages/testing/resttestclient.adoc new file mode 100644 index 000000000000..5fd807111b23 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/testing/resttestclient.adoc @@ -0,0 +1,506 @@ +[[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 + +xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and `RestTestClient` have +the same API up to the point of the call to `exchange()`. After that, `RestTestClient` +provides two alternative ways to verify the response: + +1. xref:resttestclient-workflow[Built-in Assertions] extend the request workflow with a chain of expectations +2. xref:resttestclient-assertj[AssertJ Integration] to verify the response via `assertThat()` statements + + + +[[resttestclient-workflow]] +=== Built-in Assertions + +To use the built-in assertions, remain in the workflow after the call to `exchange()`, and +use one of the expectation methods. For example: + +[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)`: 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() + .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 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() + .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`. + + +[[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() +---- +====== + + +[[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") +---- +====== + + + +[[resttestclient-assertj]] +=== AssertJ Integration + +`RestTestClientResponse` is the main entry point for the AssertJ integration. +It is an `AssertProvider` that wraps the `ResponseSpec` of an exchange in order to enable +use of `assertThat()` statements. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ResponseSpec spec = client.get().uri("/persons").exchange(); + + RestTestClientResponse response = RestTestClientResponse.from(spec); + assertThat(response).hasStatusOk(); + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); + // ... +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val spec = client.get().uri("/persons").exchange() + + val response = RestTestClientResponse.from(spec) + assertThat(response).hasStatusOk() + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) + // ... +---- +====== + +You can also use the built-in workflow first, and then obtain an `ExchangeResult` to wrap +and continue with AssertJ. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExchangeResult result = client.get().uri("/persons").exchange() + . // ... + .returnResult(); + + RestTestClientResponse response = RestTestClientResponse.from(result); + assertThat(response).hasStatusOk(); + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); + // ... +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val result = client.get().uri("/persons").exchange() + . // ... + .returnResult() + + val response = RestTestClientResponse.from(spec) + assertThat(response).hasStatusOk() + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) + // ... +---- +====== diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc index 5348b383c4cf..df049c3eb6e2 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/aot.adoc @@ -43,6 +43,16 @@ alternative, you can set the same property via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. ==== +[TIP] +==== +JPA's `@PersistenceContext` and `@PersistenceUnit` annotations cannot be used to perform +dependency injection within test classes in AOT mode. + +However, as of Spring Framework 7.0, you can inject an `EntityManager` or +`EntityManagerFactory` into tests using `@Autowired` instead of `@PersistenceContext` and +`@PersistenceUnit`, respectively. +==== + [NOTE] ==== The `@ContextHierarchy` annotation is not supported in AOT mode. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc index 659cf33a4cf4..54dc7c1262df 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/application-events.adoc @@ -2,10 +2,10 @@ = Application Events The TestContext framework provides support for recording -xref:core/beans/context-introduction.adoc#context-functionality-events[application events] published in the -`ApplicationContext` so that assertions can be performed against those events within -tests. All events published during the execution of a single test are made available via -the `ApplicationEvents` API which allows you to process the events as a +xref:core/beans/context-introduction.adoc#context-functionality-events[application events] +published in the `ApplicationContext` so that assertions can be performed against those +events within tests. All events published during the execution of a single test are made +available via the `ApplicationEvents` API which allows you to process the events as a `java.util.Stream`. 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 to be manually registered if you have custom configuration via `@TestExecutionListeners` that does not include the default listeners. -* Annotate a field of type `ApplicationEvents` with `@Autowired` and use that instance of - `ApplicationEvents` in your test and lifecycle methods (such as `@BeforeEach` and - `@AfterEach` methods in JUnit Jupiter). -** When using the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter], you may declare a method - parameter of type `ApplicationEvents` in a test or lifecycle method as an alternative - to an `@Autowired` field in the test class. +* When using the + xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[SpringExtension for JUnit Jupiter], + declare a method parameter of type `ApplicationEvents` in a `@Test`, `@BeforeEach`, or + `@AfterEach` method. +** Since `ApplicationEvents` is scoped to the lifecycle of the current test method, this + 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 -{assertj-docs}[AssertJ] to assert the types of application events -published while invoking a method in a Spring-managed component: +{assertj-docs}[AssertJ] to assert the types of application events published while +invoking a method in a Spring-managed component: // Don't use "quotes" in the "subs" section because of the asterisks in /* ... */ [tabs] @@ -38,16 +45,10 @@ Java:: @RecordApplicationEvents // <1> class OrderServiceTests { - @Autowired - OrderService orderService; - - @Autowired - ApplicationEvents events; // <2> - @Test - void submitOrder() { + void submitOrder(@Autowired OrderService service, ApplicationEvents events) { // <2> // Invoke method in OrderService that publishes an event - orderService.submitOrder(new Order(/* ... */)); + service.submitOrder(new Order(/* ... */)); // Verify that an OrderSubmitted event was published long numEvents = events.stream(OrderSubmitted.class).count(); // <3> assertThat(numEvents).isEqualTo(1); @@ -66,16 +67,10 @@ Kotlin:: @RecordApplicationEvents // <1> class OrderServiceTests { - @Autowired - lateinit var orderService: OrderService - - @Autowired - lateinit var events: ApplicationEvents // <2> - @Test - fun submitOrder() { + fun submitOrder(@Autowired service: OrderService, events: ApplicationEvents) { // <2> // Invoke method in OrderService that publishes an event - orderService.submitOrder(Order(/* ... */)) + service.submitOrder(Order(/* ... */)) // Verify that an OrderSubmitted event was published val numEvents = events.stream(OrderSubmitted::class).count() // <3> assertThat(numEvents).isEqualTo(1) diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc index a709dd96e432..055b718feaae 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/bean-overriding.adoc @@ -62,8 +62,11 @@ defined by the corresponding `BeanOverrideStrategy`: [TIP] ==== -Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean -will result in an exception. +When replacing a non-singleton bean, the non-singleton bean will be replaced with a +singleton bean corresponding to bean override instance created by the applicable +`BeanOverrideHandler`, and the corresponding bean definition will be converted to a +`singleton`. Consequently, if a handler overrides a `prototype` or scoped bean, the +overridden bean will be treated as a `singleton`. When replacing a bean created by a `FactoryBean`, the `FactoryBean` itself will be replaced with a singleton bean corresponding to bean override instance created by the diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc index b4beba546d8f..be0eda85172b 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/ctx-management/caching.adoc @@ -63,13 +63,15 @@ alternative, you can set the same property via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. 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 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 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` -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 cause the suite to take an unnecessarily long time to run, it is often beneficial to diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc index e1e03b504644..935da475f255 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/tx.adoc @@ -309,7 +309,7 @@ before-transaction method or after-transaction method runs at the appropriate ti Generally speaking, `@BeforeTransaction` and `@AfterTransaction` methods must not accept any arguments. -However, as of Spring Framework 6.1, for tests using the +However, for tests using the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] with JUnit Jupiter, `@BeforeTransaction` and `@AfterTransaction` methods may optionally accept arguments which will be resolved by any registered JUnit `ParameterResolver` diff --git a/framework-docs/modules/ROOT/pages/testing/unit.adoc b/framework-docs/modules/ROOT/pages/testing/unit.adoc index 2b360936eafa..5645587d64ef 100644 --- a/framework-docs/modules/ROOT/pages/testing/unit.adoc +++ b/framework-docs/modules/ROOT/pages/testing/unit.adoc @@ -46,8 +46,8 @@ mock objects that are useful for testing web contexts, controllers, and filters. mock objects are targeted at usage with Spring's Web MVC framework and are generally more convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock]). -TIP: Since Spring Framework 6.0, the mock objects in `org.springframework.mock.web` are -based on the Servlet 6.0 API. +TIP: Since Spring Framework 7.0, the mock objects in `org.springframework.mock.web` are +based on the Servlet 6.1 API. MockMvc builds on the mock Servlet API objects to provide an integration testing framework for Spring MVC. See xref:testing/mockmvc.adoc[MockMvc]. diff --git a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc index 4bd9e9ac06cb..7e2c642124ff 100644 --- a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc +++ b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc @@ -270,16 +270,26 @@ Kotlin:: ====== + + [[webtestclient-tests]] == Writing Tests -`WebTestClient` provides an API identical to xref:web/webflux-webclient.adoc[WebClient] -up to the point of performing a request by using `exchange()`. See the -xref:web/webflux-webclient/client-body.adoc[WebClient] documentation for examples on how to -prepare a request with any content including form data, multipart data, and more. +xref:web/webflux-webclient.adoc[WebClient] and `WebTestClient` have +the same API up to the point of the call to `exchange()`. After that, `WebTestClient` +provides two alternative ways to verify the response: + +1. xref:webtestclient-workflow[Built-in Assertions] extend the request workflow with a chain of expectations +2. xref:webtestclient-assertj[AssertJ Integration] to verify the response via `assertThat()` statements + +TIP: See the xref:web/webflux-webclient/client-body.adoc[WebClient] documentation for +examples on how to prepare a request with any content including form data, +multipart data, and more. -After the call to `exchange()`, `WebTestClient` diverges from the `WebClient` and -instead continues with a workflow to verify responses. + + +[[webtestclient-workflow]] +=== Built-in Assertions To assert the response status and headers, use the following: @@ -443,8 +453,10 @@ that accept {spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`] instead of `Class`. + + [[webtestclient-no-content]] -=== No Content +==== No Content If the response is not expected to have content, you can assert that as follows: @@ -499,8 +511,10 @@ Kotlin:: ---- ====== + + [[webtestclient-json]] -=== JSON Content +==== JSON Content You can use `expectBody()` without a target type to perform assertions on the raw content rather than through higher level Object(s). @@ -561,8 +575,10 @@ Kotlin:: ---- ====== + + [[webtestclient-stream]] -=== Streaming Responses +==== Streaming Responses To test potentially infinite streams such as `"text/event-stream"` or `"application/x-ndjson"`, start by verifying the response status and headers, and then @@ -629,6 +645,78 @@ Kotlin:: ---- ====== + + +[[webtestclient-assertj]] +=== AssertJ Integration + +`WebTestClientResponse` is the main entry point for the AssertJ integration. +It is an `AssertProvider` that wraps the `ResponseSpec` of an exchange in order to enable +use of `assertThat()` statements. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ResponseSpec spec = client.get().uri("/persons").exchange(); + + WebTestClientResponse response = WebTestClientResponse.from(spec); + assertThat(response).hasStatusOk(); + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); + // ... +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val spec = client.get().uri("/persons").exchange() + + val response = WebTestClientResponse.from(spec) + assertThat(response).hasStatusOk() + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) + // ... +---- +====== + +You can also use the built-in workflow first, and then obtain an `ExchangeResult` to wrap +and continue with AssertJ. For example: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + ExchangeResult result = client.get().uri("/persons").exchange() + . // ... + .returnResult(); + + WebTestClientResponse response = WebTestClientResponse.from(result); + assertThat(response).hasStatusOk(); + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN); + // ... +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + val result = client.get().uri("/persons").exchange() + . // ... + .returnResult() + + val response = WebTestClientResponse.from(spec) + assertThat(response).hasStatusOk() + assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN) + // ... +---- +====== + + + [[webtestclient-mockmvc]] === MockMvc Assertions diff --git a/framework-docs/modules/ROOT/pages/web-reactive.adoc b/framework-docs/modules/ROOT/pages/web-reactive.adoc index ff774a425144..eea40b37309f 100644 --- a/framework-docs/modules/ROOT/pages/web-reactive.adoc +++ b/framework-docs/modules/ROOT/pages/web-reactive.adoc @@ -3,7 +3,7 @@ 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, -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 reactive xref:web/webflux-webclient.adoc[`WebClient`], support for xref:web/webflux-test.adoc[testing], diff --git a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc index 0d2dfb0cac8a..7cca9351d908 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc @@ -294,7 +294,28 @@ allPartsEvents.windowUntil(PartEvent::isLast) ---- ====== -Note that the body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks. +NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks. + +The following shows how to bind request parameters, including an optional `DataBinder` customization: + +[tabs] +====== +Java:: ++ +[source,java] +---- +Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name")); +---- + +Kotlin:: ++ +[source,kotlin] +---- +val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")}) +---- +====== + + [[webflux-fn-response]] === ServerResponse diff --git a/framework-docs/modules/ROOT/pages/web/webflux-http-interface-client.adoc b/framework-docs/modules/ROOT/pages/web/webflux-http-service-client.adoc similarity index 53% rename from framework-docs/modules/ROOT/pages/web/webflux-http-interface-client.adoc rename to framework-docs/modules/ROOT/pages/web/webflux-http-service-client.adoc index 890478d79bb0..f083a87545ab 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-http-interface-client.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-http-service-client.adoc @@ -1,9 +1,9 @@ -[[webflux-http-interface-client]] -= HTTP Interface Client +[[webflux-http-service-client]] += HTTP Service Client The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP exchange methods. You can then generate a proxy that implements this interface and performs the exchanges. This helps to simplify HTTP remote access and provides additional -flexibility for to choose an API style such as synchronous or reactive. +flexibility in choosing an API style such as synchronous or reactive. -See xref:integration/rest-clients.adoc#rest-http-interface[REST Endpoints] for details. +See xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service Clients] for details. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc b/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc index 6c446bcb142e..070bc7798c74 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-versioning.adoc @@ -17,7 +17,7 @@ to annotated controller methods with an API version to functional endpoints with an API version Client support for API versioning is available also in `RestClient`, `WebClient`, and -xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as +xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service] clients, as well as for testing in `WebTestClient`. @@ -46,8 +46,13 @@ directly with it. [.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 -options to resolve from a header, a request parameter, or from the URL path. -You can also use a custom `ApiVersionResolver`. +options to resolve from a header, query parameter, media type parameter, +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. + diff --git a/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc b/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc index 01e2cb144a50..a8b3b595ed3e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc @@ -2,18 +2,18 @@ = WebClient :page-section-summary-toc: 1 -Spring WebFlux includes a client to perform HTTP requests with. `WebClient` has a -functional, fluent API based on Reactor, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries], +Spring WebFlux includes a client to perform HTTP requests. `WebClient` has a +functional, fluent API based on Reactor (see xref:web/webflux-reactive-libraries.adoc[Reactive Libraries]) which enables declarative composition of asynchronous logic without the need to deal with -threads or concurrency. It is fully non-blocking, it supports streaming, and relies on +threads or concurrency. It is fully non-blocking, supports streaming, and relies on the same xref:web/webflux/reactive-spring.adoc#webflux-codecs[codecs] that are also used to encode and decode request and response content on the server side. -`WebClient` needs an HTTP client library to perform requests with. There is built-in +`WebClient` needs an HTTP client library to perform requests. There is built-in support for the following: * {reactor-github-org}/reactor-netty[Reactor Netty] * {java-api}/java.net.http/java/net/http/HttpClient.html[JDK HttpClient] * https://github.com/jetty-project/jetty-reactive-httpclient[Jetty Reactive HttpClient] * https://hc.apache.org/index.html[Apache HttpComponents] -* Others can be plugged via `ClientHttpConnector`. +* Others can be plugged in via `ClientHttpConnector`. diff --git a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc index 5c5dce2a3f9c..e08eed918259 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc @@ -310,7 +310,7 @@ Java:: Flux source = ... ; Mono output = session.send(source.map(session::textMessage)); <2> - return Mono.zip(input, output).then(); <3> + return input.and(output); <3> } } ---- @@ -338,7 +338,7 @@ Kotlin:: val source: Flux = ... val output = session.send(source.map(session::textMessage)) // <2> - return Mono.zip(input, output).then() // <3> + return input.and(output) // <3> } } ---- @@ -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 of `HandshakeWebSocketService`, which performs basic checks on the WebSocket request and 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 setting a `Predicate` 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 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 functionality in the `WebSocketSession` handling to take advantage of the Tomcat-specific diff --git a/framework-docs/modules/ROOT/pages/web/webflux.adoc b/framework-docs/modules/ROOT/pages/web/webflux.adoc index ffbe046f5bd4..ffc5729b79b7 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux.adoc @@ -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, 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 -Netty, Undertow, and Servlet containers. +Netty, and Servlet containers. Both web frameworks mirror the names of their source modules ({spring-framework-code}/spring-webmvc[spring-webmvc] and diff --git a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc index 2fdd068f99e1..9f3e371686f7 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/config.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/config.adoc @@ -691,7 +691,7 @@ Java:: @Override public void configureApiVersioning(ApiVersionConfigurer configurer) { - configurer.useRequestHeader("X-API-Version"); + configurer.useRequestHeader("API-Version"); } } ---- @@ -704,7 +704,7 @@ Kotlin:: class WebConfiguration : WebMvcConfigurer { override fun configureApiVersioning(configurer: ApiVersionConfigurer) { - configurer.useRequestHeader("X-API-Version") + configurer.useRequestHeader("API-Version") } } ---- @@ -718,8 +718,12 @@ alternatively use a custom `ApiVersionResolver`: - Path segment - Media type parameter -TIP: When using a path segment, consider configuring a shared path prefix externally -in xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options. +To resolve from a path segment, you need to specify the index of the path segment expected +to contain the version. The path segment must be declared as a URI variable, e.g. +"/\{version}", "/api/\{version}", etc. where the actual name is not important. +As the version is typically at the start of the path, consider configuring it externally +as a common path prefix for all handlers through the +xref:web/webflux/config.adoc#webflux-config-path-matching[Path Matching] options. By default, the version is parsed with `SemanticVersionParser`, but you can also configure a custom xref:web/webflux-versioning.adoc#webflux-versioning-parser[ApiVersionParser]. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc index 5b4e756899ad..965c51d581e6 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc @@ -89,39 +89,7 @@ Kotlin:: You can map requests by using glob patterns and wildcards: -[cols="2,3,5"] -|=== -|Pattern |Description |Example - -| `+?+` -| Matches one character -| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+` - -| `+*+` -| 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 until the end of the path -| `+"/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. - -| `+{name}+` -| Matches a path segment and captures it as a variable named "name" -| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+` - -| `+{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 until the end of the path and captures it as a variable named "path" -| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+` - -|=== +include::partial$web/uri-patterns.adoc[leveloffset=+1] Captured URI variables can be accessed with `@PathVariable`, as the following example shows: @@ -234,10 +202,13 @@ Kotlin:: ====== -- -URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and -other property sources. You can use this, for example, to parameterize a base URL based on -some external configuration. +URI path patterns can also have: + +- Embedded `${...}` placeholders that are resolved on startup via +`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. Both classes are located in `spring-web` and are expressly designed for use with HTTP URL @@ -604,17 +575,16 @@ Kotlin:: == `@HttpExchange` [.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-httpexchange-annotation[See equivalent in the Servlet stack]# -While the main purpose of `@HttpExchange` is to abstract HTTP client code with a -generated proxy, the -xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] on which -such annotations are placed is a contract neutral to client vs server use. -In addition to simplifying client code, there are also cases where an HTTP Interface -may be a convenient way for servers to expose their API for client access. This leads -to increased coupling between client and server and is often not a good choice, -especially for public API's, but may be exactly the goal for an internal API. -It is an approach commonly used in Spring Cloud, and it is why `@HttpExchange` is -supported as an alternative to `@RequestMapping` for server side handling in -controller classes. +While the main purpose of `@HttpExchange` is for an HTTP Service +xref:integration/rest-clients.adoc#rest-http-service-client[client with a generated proxy], +the HTTP Service interface on which such annotations are placed is a contract neutral +to client vs server use. In addition to simplifying client code, there are also cases +where an HTTP Service interface may be a convenient way for servers to expose their +API for client access. This leads to increased coupling between client and server and +is often not a good choice, especially for public API's, but may be exactly the goal +for an internal API. It is an approach commonly used in Spring Cloud, and it is why +`@HttpExchange` is supported as an alternative to `@RequestMapping` for server side +handling in controller classes. For example: @@ -685,5 +655,5 @@ path, and content types. For method parameters and returns values, generally, `@HttpExchange` supports a subset of the method parameters that `@RequestMapping` does. Notably, it excludes any server-side specific parameter types. For details, see the list for -xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and +xref:integration/rest-clients.adoc#rest-http-service-client-method-parameters[@HttpExchange] and xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping]. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc index 553b9d9dac21..67fb1256a889 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-validation.adoc @@ -7,19 +7,23 @@ Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] for `@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation]. Validation may be applied at one of two levels: -1. xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], +1. Java Bean Validation is applied individually to an +xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], xref:web/webflux/controller/ann-methods/requestbody.adoc[@RequestBody], and -xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] argument -resolvers validate a method argument individually if the method parameter is annotated -with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or -`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be -discussed next). The exception raised in this case is `WebExchangeBindException`. - -2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared -directly on method parameters, or on the method (for the return value), then method -validation must be applied, and that supersedes validation at the method argument level -because method validation covers both method parameter constraints and nested constraints -via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`. +xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] method parameter +annotated with `@jakarta.validation.Valid` or Spring's `@Validated` so long as +it is a command object rather than a container such as `Map` or `Collection`, it does not +have `Errors` or `BindingResult` immediately after in the method signature, and does not +otherwise require method validation (see next). `WebExchangeBindException` is the +exception raised when validating a method parameter individually. + +2. Java Bean Validation is applied to the method when `@Constraint` annotations such as +`@Min`, `@NotBlank` and others are declared directly on method parameters, or on the +method for the return value, and it supersedes any validation that would be applied +otherwise to a method parameter individually because method validation covers both +method parameter constraints and nested constraints via `@Valid`. +`HandlerMethodValidationException` is the exception raised validation is applied +to the method. Applications must handle both `WebExchangeBindException` and `HandlerMethodValidationException` as either may be raised depending on the controller diff --git a/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc b/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc index 1b5a9e643a7e..c9a5f19080b1 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/http2.adoc @@ -4,6 +4,6 @@ [.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 {spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page]. diff --git a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc index a6dfda382304..f8f0062ceb18 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/new-framework.adoc @@ -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 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 (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 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/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. 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 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. 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]] == Performance diff --git a/framework-docs/modules/ROOT/pages/web/webflux/range.adoc b/framework-docs/modules/ROOT/pages/web/webflux/range.adoc index 7e2d5417ffe5..edcd170bd574 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/range.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/range.adoc @@ -15,6 +15,9 @@ xref:web/webflux-functional.adoc#webflux-fn-resources[serves a `Resource`]. `Ran 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`, +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` that in turn can be then written to the response via `ResourceRegionEncoder` and `ResourceHttpMessageWriter`. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc index 6301994d0bac..084ebba4aa2f 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/reactive-spring.adoc @@ -7,7 +7,7 @@ applications: * 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 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 request handling, on top of which concrete programming models such as annotated controllers and functional endpoints are built. @@ -40,10 +40,6 @@ The following table describes the supported server APIs: | Netty API | {reactor-github-org}/reactor-netty[Reactor Netty] -| Undertow -| Undertow API -| spring-web: Undertow to Reactive Streams bridge - | Tomcat | 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 @@ -67,10 +63,6 @@ The following table describes server dependencies (also see |io.projectreactor.netty |reactor-netty -|Undertow -|io.undertow -|undertow-core - |Tomcat |org.apache.tomcat.embed |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* [tabs] ====== @@ -375,10 +343,6 @@ the request, based on forwarded headers, and then removes those headers. If you it as a bean with the name `forwardedHeaderTransformer`, it will be xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[detected] and used. -NOTE: In 5.1 `ForwardedHeaderFilter` was deprecated and superseded by -`ForwardedHeaderTransformer` so forwarded headers can be processed earlier, before the -exchange is created. If the filter is configured anyway, it is taken out of the list of -filters, and `ForwardedHeaderTransformer` is used instead. [[webflux-forwarded-headers-security]] === Security Considerations @@ -581,6 +545,20 @@ The `ProtobufJsonDecoder` and `ProtobufJsonEncoder` variants support reading and They require the "com.google.protobuf:protobuf-java-util" dependency. Note, the JSON variants do not support reading stream of messages, see the {spring-framework-api}/http/codec/protobuf/ProtobufJsonDecoder.html[javadoc of `ProtobufJsonDecoder`] for more details. +[[webflux-codecs-gson]] +=== Google Gson + +Applications can use the `GsonEncoder` and `GsonDecoder` to serialize and deserialize JSON documents thanks to the https://google.github.io/gson/[Google Gson] library . +This codec supports both JSON media types and the NDJSON format for streaming. + +[NOTE] +==== +`Gson` does not support non-blocking parsing, so the `GsonDecoder` does not support deserializing +to `Flux<*>` types. For example, if this decoder is used for deserializing a JSON stream or even a list of elements +as a `Flux<*>`, an `UnsupportedOperationException` will be thrown at runtime. +Applications should instead focus on deserializing bounded collections and use `Mono>` as target types. +==== + [[webflux-codecs-limits]] === Limits diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc index 17bd4bcd567b..03b8950d7e40 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-client.adoc @@ -15,27 +15,27 @@ See xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] for more de [[webmvc-webclient]] == `WebClient` -`WebClient` is a reactive client to perform HTTP requests with a fluent API. +`WebClient` is a reactive client for making HTTP requests with a fluent API. -See xref:web/webflux-webclient.adoc[WebClient] for more details. +See xref:web/webflux-webclient.adoc[`WebClient`] for more details. [[webmvc-resttemplate]] == `RestTemplate` -`RestTemplate` is a synchronous client to perform HTTP requests. It is the original +`RestTemplate` is a synchronous client for making HTTP requests. It is the original Spring REST client and exposes a simple, template-method API over underlying HTTP client libraries. -See xref:integration/rest-clients.adoc[REST Endpoints] for details. +See xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] for details. -[[webmvc-http-interface]] -== HTTP Interface +[[webmvc-http-service-client]] +== HTTP Service Client -The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP +The Spring Framework lets you define an HTTP service as a Java interface with HTTP exchange methods. You can then generate a proxy that implements this interface and performs the exchanges. This helps to simplify HTTP remote access and provides additional -flexibility for to choose an API style such as synchronous or reactive. +flexibility for choosing an API style such as synchronous or reactive. -See xref:integration/rest-clients.adoc#rest-http-interface[REST Endpoints] for details. +See xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service Client] for details. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc index 141a5c7d00a8..5901b99fb945 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc @@ -184,6 +184,26 @@ val map = request.params() ---- ====== +The following shows how to bind request parameters, including an optional `DataBinder` customization: + +[tabs] +====== +Java:: ++ +[source,java] +---- +Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name")); +---- + +Kotlin:: ++ +[source,kotlin] +---- +val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")}) +---- +====== + + [[webmvc-fn-response]] === ServerResponse diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc index 2be54cd83351..6d548ad356b9 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-versioning.adoc @@ -16,7 +16,7 @@ to annotated controller methods with an API version to functional endpoints with an API version Client support for API versioning is available also in `RestClient`, `WebClient`, and -xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as +xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service] clients, as well as for testing in MockMvc and `WebTestClient`. @@ -46,8 +46,13 @@ directly with it. [.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 -options to resolve from a header, from a request parameter, or from the URL path. -You can also use a custom `ApiVersionResolver`. +options to resolve from a header, query parameter, media type parameter, +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. + diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-document.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-document.adoc index 73e1e7c27e52..d1e50c6e53a7 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-document.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-document.adoc @@ -4,10 +4,14 @@ Spring offers ways to return output other than HTML, including PDF and Excel spreadsheets. This section describes how to use those features. +WARNING: As of Spring Framework 7.0, view classes in the `org.springframework.web.servlet.view.document` +package are deprecated. Instead, libraries can adapt this existing code to provide support with their own `*View` types. +As an alternative, applications can perform direct rendering in web handlers. [[mvc-view-document-intro]] == Introduction to Document Views + An HTML page is not always the best way for the user to view the model output, and Spring makes it simple to generate a PDF document or an Excel spreadsheet dynamically from the model data. The document is the view and is streamed from the diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc index 825dfd46607c..78069fc588c2 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-feeds.adoc @@ -1,6 +1,10 @@ [[mvc-view-feeds]] = RSS and Atom +WARNING: As of Spring Framework 7.0, view classes in the `org.springframework.web.servlet.view.feed` +package are deprecated. Instead, libraries can adapt this existing code to provide support with their own `*View` types. +As an alternative, applications can perform direct rendering in web handlers. + Both `AbstractAtomFeedView` and `AbstractRssFeedView` inherit from the `AbstractFeedView` base class and are used to provide Atom and RSS Feed views, respectively. They are based on https://rometools.github.io/rome/[ROME] project and are located in the diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc index 6ca828bef563..7eb33c7180ac 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-jsp.adoc @@ -11,16 +11,42 @@ When developing with JSPs, you typically declare an `InternalResourceViewResolve `InternalResourceViewResolver` can be used for dispatching to any Servlet resource but in particular for JSPs. As a best practice, we strongly encourage placing your JSP files in -a directory under the `'WEB-INF'` directory so there can be no direct access by clients. +a directory under the `WEB-INF` directory so there can be no direct access by clients. +This is what is done by the configuration below which registers a JSP view resolver using +a default view name prefix of `"/WEB-INF/"` and a default suffix of `".jsp"`. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- +@EnableWebMvc +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.jsp(); + } +} +---- + +XML:: ++ [source,xml,indent=0,subs="verbatim,quotes"] ---- - - - - - + + + + + ---- +====== + +[NOTE] +You can specify custom prefix and suffix. [[mvc-view-jsp-jstl]] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc index d2813753a95e..490f5d9e623b 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc @@ -3,7 +3,10 @@ [.small]#xref:web/webflux/reactive-spring.adoc#webflux-filters[See equivalent in the Reactive stack]# -The `spring-web` module provides some useful filters: +In the Servlet API, you can add a `jakarta.servlet.Filter` to apply interception-style logic +before and after the rest of the processing chain of filters and the target `Servlet`. + +The `spring-web` module has a number of built-in `Filter` implementations: * xref:web/webmvc/filters.adoc#filters-http-put[Form Data] * xref:web/webmvc/filters.adoc#filters-forwarded-headers[Forwarded Headers] @@ -11,9 +14,19 @@ The `spring-web` module provides some useful filters: * xref:web/webmvc/filters.adoc#filters-cors[CORS] * xref:web/webmvc/filters.adoc#filters.url-handler[URL Handler] -Servlet filters can be configured in the `web.xml` configuration file or using Servlet annotations. -If you are using Spring Boot, you can -{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare them as beans and configure them as part of your application]. +There are also base class implementations for use in Spring applications: + +* `GenericFilterBean` -- base class for a `Filter` configured as a Spring bean; +integrates with the Spring `ApplicationContext` lifecycle. +* `OncePerRequestFilter` -- extension of `GenericFilterBean` that supports a single +invocation at the start of a request, i.e. during the `REQUEST` dispatch phase, and +ignoring further handling via `FORWARD` dispatches. The filter also provides control +over whether the `Filter` gets involved in `ASYNC` and `ERROR` dispatches. + +Servlet filters can be configured in `web.xml` or via Servlet annotations. +In a Spring Boot application , you can +{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare Filter's as beans] +and Boot will have them configured. [[filters-http-put]] diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc index ea0a9ce87f61..b44f0828ade8 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-async.adoc @@ -422,7 +422,7 @@ reactive types from the controller method. Reactive return values are handled as follows: * A single-value promise is adapted to, similar to using `DeferredResult`. Examples -include `CompletionStage` (JDK), Mono` (Reactor), and `Single` (RxJava). +include `CompletionStage` (JDK), `Mono` (Reactor), and `Single` (RxJava). * A multi-value stream with a streaming media type (such as `application/x-ndjson` or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or `SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava). diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc index c989de0dcdd5..0f221f33a8ee 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/api-version.adoc @@ -15,8 +15,12 @@ alternatively use a custom `ApiVersionResolver`: - Path segment - Media type parameter -TIP: When using a path segment, consider configuring a shared path prefix externally -in xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options. +To resolve from a path segment, you need to specify the index of the path segment expected +to contain the version. The path segment must be declared as a URI variable, e.g. +"/\{version}", "/api/\{version}", etc. where the actual name is not important. +As the version is typically at the start of the path, consider configuring it externally +as a common path prefix for all handlers through the +xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options. By default, the version is parsed with `SemanticVersionParser`, but you can also configure a custom xref:web/webmvc-versioning.adoc#mvc-versioning-parser[ApiVersionParser]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc index 94ff1b4f420b..9659c62f1cf8 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc @@ -88,37 +88,17 @@ Kotlin:: == URI patterns [.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: - -* `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. -* `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. - -`PathPattern` is the recommended solution for web applications and it is the only choice in -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. +`@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` supports the same pattern syntax as `AntPathMatcher`. In addition, it also -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 -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]. +NOTE: the `AntPathMatcher` variant is now deprecated because it is less efficient and the String path input is a +challenge for dealing effectively with encoding and other issues with URLs. -Some example patterns: +You can map requests by using glob patterns and wildcards: -* `+"/resources/ima?e.png"+` - match one character in a path segment -* `+"/resources/*.png"+` - match zero or more characters in a path segment -* `+"/resources/**"+` - match multiple path segments -* `+"/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 +include::partial$web/uri-patterns.adoc[leveloffset=+1] Captured URI variables can be accessed with `@PathVariable`. For example: @@ -217,10 +197,13 @@ Kotlin:: ---- ====== -URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and -other property sources. You can use this, for example, to parameterize a base URL based on -some external configuration. +URI path patterns can also have: + +- Embedded `${...}` placeholders that are resolved on startup via +`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]] @@ -622,10 +605,9 @@ Kotlin:: [.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-httpexchange-annotation[See equivalent in the Reactive stack]# While the main purpose of `@HttpExchange` is to abstract HTTP client code with a -generated proxy, the -xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] on which -such annotations are placed is a contract neutral to client vs server use. -In addition to simplifying client code, there are also cases where an HTTP Interface +generated proxy, the interface on which such annotations are placed is a contract neutral +to client vs server use. In addition to simplifying client code, there are also cases +where an xref:integration/rest-clients.adoc#rest-http-service-client[HTTP Service Client] may be a convenient way for servers to expose their API for client access. This leads to increased coupling between client and server and is often not a good choice, especially for public API's, but may be exactly the goal for an internal API. @@ -702,7 +684,7 @@ path, and content types. For method parameters and returns values, generally, `@HttpExchange` supports a subset of the method parameters that `@RequestMapping` does. Notably, it excludes any server-side specific parameter types. For details, see the list for -xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and +xref:integration/rest-clients.adoc#rest-http-service-client-method-parameters[@HttpExchange] and xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[@RequestMapping]. `@HttpExchange` also supports a `headers()` parameter which accepts `"name=value"`-like diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc index 99ddf8635eb1..45e5c632f4b7 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-validation.adoc @@ -7,22 +7,26 @@ Spring MVC has built-in xref:core/validation/validator.adoc[validation] for `@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation]. Validation may be applied at one of two levels: -1. xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], +1. Java Bean Validation is applied individually to an +xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute], xref:web/webmvc/mvc-controller/ann-methods/requestbody.adoc[@RequestBody], and -xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] argument -resolvers validate a method argument individually if the method parameter is annotated -with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or -`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be -discussed next). The exception raised in this case is `MethodArgumentNotValidException`. - -2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared -directly on method parameters, or on the method (for the return value), then method -validation must be applied, and that supersedes validation at the method argument level -because method validation covers both method parameter constraints and nested constraints -via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`. - -Applications must handle both `MethodArgumentNotValidException` and -`HandlerMethodValidationException` as either may be raised depending on the controller +xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] method parameter +annotated with `@jakarta.validation.Valid` or Spring's `@Validated` so long as +it is a command object rather than a container such as `Map` or `Collection`, it does not +have `Errors` or `BindingResult` immediately after in the method signature, and does not +otherwise require method validation (see next). `MethodArgumentNotValidException` is the +exception raised when validating a method parameter individually. + +2. Java Bean Validation is applied to the method when `@Constraint` annotations such as +`@Min`, `@NotBlank` and others are declared directly on method parameters, or on the +method for the return value, and it supersedes any validation that would be applied +otherwise to a method parameter individually because method validation covers both +method parameter constraints and nested constraints via `@Valid`. +`HandlerMethodValidationException` is the exception raised validation is applied +to the method. + +Applications should handle both `MethodArgumentNotValidException` and +`HandlerMethodValidationException` since either may be raised depending on the controller method signature. The two exceptions, however are designed to be very similar, and can be handled with almost identical code. The main difference is that the former is for a single object while the latter is for a list of method parameters. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-range.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-range.adoc index f47c85cb79e6..7ba60ead5379 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-range.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-range.adoc @@ -15,6 +15,9 @@ xref:web/webmvc-functional.adoc#webmvc-fn-resources[serves a `Resource`]. `Range 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`, +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` that in turn can be then written to the response via `ResourceRegionHttpMessageConverter`. \ No newline at end of file diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc index 75dbbb1919b3..cdf564f281d8 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/multipart.adoc @@ -6,8 +6,6 @@ `MultipartResolver` from the `org.springframework.web.multipart` package is a strategy for parsing multipart requests including file uploads. There is a container-based `StandardServletMultipartResolver` implementation for Servlet multipart request parsing. -Note that the outdated `CommonsMultipartResolver` based on Apache Commons FileUpload is -not available anymore, as of Spring Framework 6.0 with its new Servlet 5.0+ baseline. To enable multipart handling, you need to declare a `MultipartResolver` bean in your `DispatcherServlet` Spring configuration with a name of `multipartResolver`. diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc index 1456a7e07703..c1706a321d38 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/configuration-performance.adoc @@ -72,7 +72,7 @@ such as https://github.com/stomp-js/stompjs[`stomp-js/stompjs`] and others split STOMP messages at 16K boundaries and send them as multiple WebSocket messages, which requires the server to buffer and re-assemble. -Spring's STOMP-over-WebSocket support does this ,so applications can configure the +Spring's STOMP-over-WebSocket support does this, so applications can configure the maximum size for STOMP messages irrespective of WebSocket server-specific message sizes. Keep in mind that the WebSocket message size is automatically adjusted, if necessary, to ensure they can carry 16K WebSocket messages at a diff --git a/framework-docs/modules/ROOT/pages/web/websocket/stomp/scope.adoc b/framework-docs/modules/ROOT/pages/web/websocket/stomp/scope.adoc index b066e00e56a5..0be9eecb94a4 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/stomp/scope.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/stomp/scope.adoc @@ -1,8 +1,8 @@ [[websocket-stomp-websocket-scope]] = WebSocket Scope -Each WebSocket session has a map of attributes. The map is attached as a header to -inbound client messages and may be accessed from a controller method, as the following example shows: +Each WebSocket session has a map of attributes. The map is attached as a header to inbound +client messages and may be accessed from a controller method, as the following example shows: [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 inject WebSocket-scoped beans into controllers and any channel interceptors registered on the `clientInboundChannel`. Those are typically singletons and live -longer than any individual WebSocket session. Therefore, you need to use a -scope proxy mode for WebSocket-scoped beans, as the following example shows: +longer than any individual WebSocket session. Therefore, you need to use +WebSocket-scoped beans in proxy mode, conveniently defined with `@WebSocketScope`: [source,java,indent=0,subs="verbatim,quotes"] ---- @Component - @Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) + @WebSocketScope public class MyBean { @PostConstruct diff --git a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc index 45e48114a618..c1f63fad0de0 100644 --- a/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc +++ b/framework-docs/modules/ROOT/partials/web/forwarded-headers.adoc @@ -9,7 +9,7 @@ that proxies can use to provide information about the original request. === Non-standard Headers There are other non-standard headers, too, including `X-Forwarded-Host`, `X-Forwarded-Port`, -`X-Forwarded-Proto`, `X-Forwarded-Ssl`, and `X-Forwarded-Prefix`. +`X-Forwarded-Proto`, `X-Forwarded-Ssl`, `X-Forwarded-Prefix`, and `X-Forwarded-For`. [[x-forwarded-host]] ==== X-Forwarded-Host @@ -113,3 +113,12 @@ https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path} In this case, the proxy has a prefix of `/api/app1` and the server has a prefix of `/app1`. The proxy can send `X-Forwarded-Prefix: /api/app1` to have the original prefix `/api/app1` override the server prefix `/app1`. + +[[x-forwarded-for]] +==== X-Forwarded-For + +https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For[`X-Forwarded-For:

`] +is a de-facto standard header that is used to communicate the original `InetSocketAddress` of the client to a +downstream server. For example, if a request is sent by a client at `[fd00:fefe:1::4]` to a proxy at +`192.168.0.1`, the "remote address" information contained in the HTTP request will reflect the actual address of the +client, not the proxy. diff --git a/framework-docs/modules/ROOT/partials/web/uri-patterns.adoc b/framework-docs/modules/ROOT/partials/web/uri-patterns.adoc new file mode 100644 index 000000000000..494c50a6c0a1 --- /dev/null +++ b/framework-docs/modules/ROOT/partials/web/uri-patterns.adoc @@ -0,0 +1,51 @@ +[cols="2,3,5"] +|=== +|Pattern |Description |Example + +| `spring` +| Literal pattern +| `+"/spring"+` matches `+"/spring"+` + +| `+?+` +| Matches one character +| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+` + +| `+*+` +| 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"+`. + +`+"/projects/*"+` matches `+"/projects/spring"+` but does not match `+"/projects"+` as the path segment is not present. + +| `+**+` +| Matches zero or more path segments +| `+"/resources/**"+` matches `+"/resources"+`, `+"/resources/file.png"+` and `+"/resources/images/file.png"+` + +`+"/**/info"+` matches `+"/info"+`, `+"/spring/info"+` and `+"/spring/framework/info"+` + +`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path. + +`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern. + +| `+{name}+` +| Similar to `+*+`, but also captures the path segment 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}+` +| Similar to `+**+`, but also captures the path segments 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}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern. + +|=== \ No newline at end of file diff --git a/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.java b/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.java new file mode 100644 index 000000000000..22fb1c08c4b3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.java @@ -0,0 +1,53 @@ +/* + * 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.core.beans.java.beansjavaprogrammaticregistration; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; + +// tag::snippet[] +class MyBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class); + registry.registerBean("bar", Bar.class, spec -> spec + .prototype() + .lazyInit() + .description("Custom description") + .supplier(context -> new Bar(context.bean(Foo.class)))); + if (env.matchesProfiles("baz")) { + registry.registerBean(Baz.class, spec -> spec + .supplier(context -> new Baz("Hello World!"))); + } + registry.registerBean(MyRepository.class); + registry.registerBean(RouterFunction.class, spec -> + spec.supplier(context -> router(context.bean(MyRepository.class)))); + } + + RouterFunction router(MyRepository myRepository) { + return RouterFunctions.route() + // ... + .build(); + } + +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.java new file mode 100644 index 000000000000..8593224eacee --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.java @@ -0,0 +1,27 @@ +/* + * 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.core.beans.java.beansjavaprogrammaticregistration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +// tag::snippet[] +@Configuration +@Import(MyBeanRegistrar.class) +class MyConfiguration { +} +// end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssending/JmsQueueSender.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssending/JmsQueueSender.java new file mode 100644 index 000000000000..9b8aede1b8b9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssending/JmsQueueSender.java @@ -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"); + } + }); + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingconversion/JmsSenderWithConversion.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingconversion/JmsSenderWithConversion.java new file mode 100644 index 000000000000..a9f43e2c5f08 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingconversion/JmsSenderWithConversion.java @@ -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 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; + } + }); + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingjmsclient/JmsClientSample.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingjmsclient/JmsClientSample.java new file mode 100644 index 000000000000..3f7468c839c3 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingjmsclient/JmsClientSample.java @@ -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); + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingpostprocessor/JmsClientWithPostProcessor.java b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingpostprocessor/JmsClientWithPostProcessor.java new file mode 100644 index 000000000000..2e5a4a4a634a --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/jms/jmssendingpostprocessor/JmsClientWithPostProcessor.java @@ -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(); + } + } +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.java b/framework-docs/src/main/java/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.java similarity index 95% rename from framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.java rename to framework-docs/src/main/java/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.java index 3702dc51048b..6982f682d0c4 100644 --- a/framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.java +++ b/framework-docs/src/main/java/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.docs.integration.resthttpinterface.customresolver; +package org.springframework.docs.integration.resthttpserviceclient.customresolver; import java.util.List; @@ -28,14 +28,14 @@ public class CustomHttpServiceArgumentResolver { - // tag::httpinterface[] + // tag::httpserviceclient[] public interface RepositoryService { @GetExchange("/repos/search") List searchRepository(Search search); } - // end::httpinterface[] + // end::httpserviceclient[] class Sample { diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java index a58293146da2..d1baeae21328 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java @@ -26,7 +26,7 @@ public class WebConfiguration implements WebMvcConfigurer { @Override public void configureApiVersioning(ApiVersionConfigurer configurer) { - configurer.useRequestHeader("X-API-Version"); + configurer.useRequestHeader("API-Version"); } } // end::snippet[] diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java index cfdba475159e..f5290333bd3a 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.java @@ -44,8 +44,8 @@ public void configureMessageConverters(HttpMessageConverters.ServerBuilder build .findAndAddModules() .defaultUseWrapper(false) .build(); - builder.jsonMessageConverter(new JacksonJsonHttpMessageConverter(jsonMapper)) - .xmlMessageConverter(new JacksonXmlHttpMessageConverter(xmlMapper)); + builder.withJsonConverter(new JacksonJsonHttpMessageConverter(jsonMapper)) + .withXmlConverter(new JacksonXmlHttpMessageConverter(xmlMapper)); } } // end::snippet[] diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Bar.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Bar.kt new file mode 100644 index 000000000000..eeeb6a546288 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Bar.kt @@ -0,0 +1,19 @@ +/* + * 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.core.beans.java.beansjavaprogrammaticregistration + +data class Bar(val foo: Foo) \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Baz.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Baz.kt new file mode 100644 index 000000000000..0dab54a5c545 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Baz.kt @@ -0,0 +1,19 @@ +/* + * 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.core.beans.java.beansjavaprogrammaticregistration + +data class Baz(val value: String) \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Foo.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Foo.kt new file mode 100644 index 000000000000..0942b2193a5b --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/Foo.kt @@ -0,0 +1,19 @@ +/* + * 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.core.beans.java.beansjavaprogrammaticregistration + +class Foo \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.kt new file mode 100644 index 000000000000..51a898fb7f46 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyBeanRegistrar.kt @@ -0,0 +1,44 @@ +/* + * 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.core.beans.java.beansjavaprogrammaticregistration + +import org.springframework.beans.factory.BeanRegistrarDsl +import org.springframework.web.servlet.function.router + +// tag::snippet[] +class MyBeanRegistrar : BeanRegistrarDsl({ + registerBean() + registerBean( + name = "bar", + prototype = true, + lazyInit = true, + description = "Custom description") { + Bar(bean()) // Also possible with Bar(bean()) + } + profile("baz") { + registerBean { Baz("Hello World!") } + } + registerBean() + registerBean { + myRouter(bean()) // Also possible with myRouter(bean()) + } +}) + +fun myRouter(myRepository: MyRepository) = router { + // ... +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.kt new file mode 100644 index 000000000000..81f7c29fc84e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyConfiguration.kt @@ -0,0 +1,27 @@ +/* + * 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.core.beans.java.beansjavaprogrammaticregistration + +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import + +// tag::snippet[] +@Configuration +@Import(MyBeanRegistrar::class) +class MyConfiguration { +} +// end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyRepository.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyRepository.kt new file mode 100644 index 000000000000..e40ad268baf9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/java/beansjavaprogrammaticregistration/MyRepository.kt @@ -0,0 +1,20 @@ +/* + * 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.core.beans.java.beansjavaprogrammaticregistration + +interface MyRepository { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.kt similarity index 94% rename from framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt rename to framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.kt index b1412985d7e7..8f608e2d182c 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpserviceclient/customresolver/CustomHttpServiceArgumentResolver.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.docs.integration.resthttpinterface.customresolver +package org.springframework.docs.integration.resthttpserviceclient.customresolver import org.springframework.core.MethodParameter import org.springframework.web.client.RestClient @@ -26,14 +26,14 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory class CustomHttpServiceArgumentResolver { - // tag::httpinterface[] + // tag::httpserviceclient[] interface RepositoryService { @GetExchange("/repos/search") fun searchRepository(search: Search): List } - // end::httpinterface[] + // end::httpserviceclient[] class Sample { fun sample() { diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/languages/kotlin/coroutines/propagation/ContextPropagationSample.kt b/framework-docs/src/main/kotlin/org/springframework/docs/languages/kotlin/coroutines/propagation/ContextPropagationSample.kt new file mode 100644 index 000000000000..0de429ba57f6 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/languages/kotlin/coroutines/propagation/ContextPropagationSample.kt @@ -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.languages.kotlin.coroutines.propagation + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory +import org.springframework.core.PropagationContextElement + +class ContextPropagationSample { + + companion object { + private val logger: Log = LogFactory.getLog( + ContextPropagationSample::class.java + ) + } + + // tag::context[] + fun main() { + runBlocking(Dispatchers.IO + PropagationContextElement()) { + waitAndLog() + } + } + + suspend fun waitAndLog() { + delay(10) + logger.info("Suspending function with traceId") + } + // end::context[] +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt index dec34ad91964..4a315aef00be 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt @@ -25,7 +25,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer class WebConfiguration : WebMvcConfigurer { override fun configureApiVersioning(configurer: ApiVersionConfigurer) { - configurer.useRequestHeader("X-API-Version") + configurer.useRequestHeader("API-Version") } } // end::snippet[] \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt index 476f717b4624..854f43ff7677 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigmessageconverters/WebConfiguration.kt @@ -26,8 +26,8 @@ class WebConfiguration : WebMvcConfigurer { .findAndAddModules() .defaultUseWrapper(false) .build() - builder.jsonMessageConverter(JacksonJsonHttpMessageConverter(jsonMapper)) - .xmlMessageConverter(JacksonXmlHttpMessageConverter(xmlMapper)) + builder.withJsonConverter(JacksonJsonHttpMessageConverter(jsonMapper)) + .withXmlConverter(JacksonXmlHttpMessageConverter(xmlMapper)) } } // end::snippet[] \ No newline at end of file diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index 83a2031f5c64..6d2c0cbc5d96 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -7,37 +7,37 @@ javaPlatform { } dependencies { - api(platform("com.fasterxml.jackson:jackson-bom:2.18.4")) - api(platform("io.micrometer:micrometer-bom:1.16.0-M1")) - api(platform("io.netty:netty-bom:4.2.3.Final")) - api(platform("io.projectreactor:reactor-bom:2025.0.0-M5")) + api(platform("com.fasterxml.jackson:jackson-bom:2.20.0")) + api(platform("io.micrometer:micrometer-bom:1.16.0-RC1")) + api(platform("io.netty:netty-bom:4.2.7.Final")) + api(platform("io.projectreactor:reactor-bom:2025.0.0")) api(platform("io.rsocket:rsocket-bom:1.1.5")) - api(platform("org.apache.groovy:groovy-bom:4.0.27")) - api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3")) - 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.ee11:jetty-ee11-bom:12.1.0.beta1")) + api(platform("org.apache.groovy:groovy-bom:5.0.2")) + api(platform("org.apache.logging.log4j:log4j-bom:2.25.1")) + api(platform("org.assertj:assertj-bom:3.27.6")) + api(platform("org.eclipse.jetty:jetty-bom:12.1.3")) + api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.3")) 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.junit:junit-bom:5.13.3")) - api(platform("org.mockito:mockito-bom:5.18.0")) - api(platform("tools.jackson:jackson-bom:3.0.0-rc5")) + api(platform("org.junit:junit-bom:6.0.1")) + api(platform("org.mockito:mockito-bom:5.20.0")) + api(platform("tools.jackson:jackson-bom:3.0.1")) constraints { - api("com.fasterxml:aalto-xml:1.3.2") + api("com.fasterxml:aalto-xml:1.3.4") 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.3") api("com.github.librepdf:openpdf:1.3.43") api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:jsr305:3.0.2") - api("com.google.code.gson:gson:2.13.1") - api("com.google.protobuf:protobuf-java-util:4.30.2") + api("com.google.code.gson:gson:2.13.2") + api("com.google.protobuf:protobuf-java-util:4.32.1") api("com.h2database:h2:2.3.232") api("com.jayway.jsonpath:json-path:2.9.0") api("com.networknt:json-schema-validator:1.5.3") api("com.oracle.database.jdbc:ojdbc11:21.9.0.0") api("com.rometools:rome:1.19.0") - api("com.squareup.okhttp3:mockwebserver3:5.1.0") + api("com.squareup.okhttp3:mockwebserver3:5.3.0") 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-impl:3.0.2") @@ -47,16 +47,13 @@ dependencies { api("commons-io:commons-io:2.15.0") api("commons-logging:commons-logging:1.3.5") api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") - api("io.mockk:mockk:1.14.4") + api("io.mockk:mockk:1.14.5") api("io.projectreactor.tools:blockhound:1.0.8.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:1.0.0.RELEASE") - api("io.reactivex.rxjava3:rxjava:3.1.10") + api("io.reactivex.rxjava3:rxjava:3.1.12") 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("jakarta.activation:jakarta.activation-api:2.1.3") api("jakarta.annotation:jakarta.annotation-api:3.0.0") @@ -88,22 +85,22 @@ dependencies { api("junit:junit:4.13.2") api("net.sf.jopt-simple:jopt-simple:5.0.4") api("org.apache-extras.beanshell:bsh:2.0b6") - api("org.apache.activemq:activemq-broker:5.17.6") - api("org.apache.activemq:activemq-kahadb-store:5.17.6") - api("org.apache.activemq:activemq-stomp:5.17.6") - api("org.apache.activemq:artemis-jakarta-client:2.31.2") - api("org.apache.activemq:artemis-junit-5:2.31.2") - api("org.apache.commons:commons-pool2:2.9.0") + api("org.apache.activemq:activemq-broker:5.17.7") + api("org.apache.activemq:activemq-kahadb-store:5.17.7") + api("org.apache.activemq:activemq-stomp:5.17.7") + api("org.apache.activemq:artemis-jakarta-client:2.42.0") + api("org.apache.activemq:artemis-junit-5:2.42.0") + api("org.apache.commons:commons-pool2:2.12.1") api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1") api("org.apache.httpcomponents.client5:httpclient5:5.5") - api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4") + api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.5") api("org.apache.poi:poi-ooxml:5.2.5") - api("org.apache.tomcat.embed:tomcat-embed-core:11.0.7") - api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.7") - api("org.apache.tomcat:tomcat-util:11.0.7") - api("org.apache.tomcat:tomcat-websocket:11.0.7") + api("org.apache.tomcat.embed:tomcat-embed-core:11.0.13") + api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.13") + api("org.apache.tomcat:tomcat-util:11.0.13") + api("org.apache.tomcat:tomcat-websocket:11.0.13") api("org.aspectj:aspectjrt:1.9.24") api("org.aspectj:aspectjtools:1.9.24") api("org.aspectj:aspectjweaver:1.9.24") @@ -111,11 +108,11 @@ dependencies { api("org.bouncycastle:bcpkix-jdk18on:1.72") api("org.codehaus.jettison:jettison:1.5.4") api("org.crac:crac:1.4.0") - api("org.dom4j:dom4j:2.1.4") - api("org.easymock:easymock:5.5.0") + api("org.dom4j:dom4j:2.2.0") + api("org.easymock:easymock:5.6.0") api("org.eclipse.angus:angus-mail:2.0.3") - api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.9") - api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-B08") + api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.0") + api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-B11") api("org.eclipse:yasson:3.0.4") api("org.ehcache:ehcache:3.10.8") api("org.ehcache:jcache:1.0.1") @@ -124,13 +121,13 @@ dependencies { api("org.glassfish:jakarta.el:4.0.2") api("org.graalvm.sdk:graal-sdk:22.3.1") api("org.hamcrest:hamcrest:3.0") - api("org.hibernate.orm:hibernate-core:7.0.5.Final") - api("org.hibernate.validator:hibernate-validator:9.0.1.Final") + api("org.hibernate.orm:hibernate-core:7.2.0.CR1") + api("org.hibernate.validator:hibernate-validator:9.1.0.Final") api("org.hsqldb:hsqldb:2.7.4") - api("org.htmlunit:htmlunit:4.13.0") + api("org.htmlunit:htmlunit:4.18.0") api("org.javamoney:moneta:1.4.4") 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.junit.support:testng-engine:1.0.5") api("org.mozilla:rhino:1.7.15") @@ -138,14 +135,14 @@ dependencies { api("org.python:jython-standalone:2.7.4") api("org.quartz-scheduler:quartz:2.3.2") api("org.reactivestreams:reactive-streams:1.0.4") - api("org.seleniumhq.selenium:htmlunit3-driver:4.33.0") - api("org.seleniumhq.selenium:selenium-java:4.34.0") - api("org.skyscreamer:jsonassert:2.0-rc1") + api("org.seleniumhq.selenium:htmlunit3-driver:4.38.0") + api("org.seleniumhq.selenium:selenium-java:4.38.0") + api("org.skyscreamer:jsonassert:1.5.3") api("org.testng:testng:7.11.0") api("org.webjars:underscorejs:1.8.3") api("org.webjars:webjars-locator-lite:1.1.0") - api("org.xmlunit:xmlunit-assertj:2.10.3") - api("org.xmlunit:xmlunit-matchers:2.10.3") - api("org.yaml:snakeyaml:2.4") + api("org.xmlunit:xmlunit-assertj:2.10.4") + api("org.xmlunit:xmlunit-matchers:2.10.4") + api("org.yaml:snakeyaml:2.5") } } diff --git a/gradle.properties b/gradle.properties index 8d3de9c13196..e5e2aa8ff3f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,11 +4,8 @@ org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true -kotlinVersion=2.2.0 +kotlinVersion=2.2.21 byteBuddyVersion=1.17.6 kotlin.jvm.target.validation.mode=ignore kotlin.stdlib.default.dependency=false - -org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true -org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled \ No newline at end of file diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index a899900abf56..09755cdac4a0 100644 --- a/gradle/spring-module.gradle +++ b/gradle/spring-module.gradle @@ -78,7 +78,7 @@ javadoc { // Change modularity mismatch from warn to info. // See https://github.com/spring-projects/spring-framework/issues/27497 addStringOption("-link-modularity-mismatch", "info") - // With the javadoc tool on Java 24, it appears that the 'reference' + // With the javadoc tool on Java 25, it appears that the 'reference' // group is always active and the '-reference' flag is not honored. // Thus, we do NOT fail the build on Javadoc warnings due to // cross-module @see and @link references which are only reachable @@ -118,4 +118,3 @@ publishing { // Disable publication of test fixture artifacts. components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() } components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } - diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55baabb..f8e1ee3125fe 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da476bb..bad7c2462f5a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 23d15a936707..adff685a0348 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index 5eed7ee84528..e509b2dd8fe5 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java index 0df3dc5c0f72..bf6c07c8173f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java @@ -39,13 +39,13 @@ class AspectJAdvisorBeanRegistrationAotProcessor implements BeanRegistrationAotP private static final String AJC_MAGIC = "ajc$"; - private static final boolean aspectjPresent = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", + private static final boolean ASPECTJ_PRESENT = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", AspectJAdvisorBeanRegistrationAotProcessor.class.getClassLoader()); @Override public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { - if (aspectjPresent) { + if (ASPECTJ_PRESENT) { Class beanClass = registeredBean.getBeanClass(); if (compiledByAjc(beanClass)) { return new AspectJAdvisorContribution(beanClass); diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java index 28c2408757a7..32ecda58883a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java @@ -41,13 +41,13 @@ */ class AspectJBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { - private static final boolean aspectJPresent = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", + private static final boolean ASPECTJ_PRESENT = ClassUtils.isPresent("org.aspectj.lang.annotation.Pointcut", AspectJBeanFactoryInitializationAotProcessor.class.getClassLoader()); @Override public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { - if (aspectJPresent) { + if (ASPECTJ_PRESENT) { return AspectDelegate.processAheadOfTime(beanFactory); } return null; diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java index a7a9e033d1f7..3247fa213d39 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java @@ -23,6 +23,8 @@ import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; 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.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -96,17 +98,22 @@ public abstract class AopConfigUtils { } public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { - if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); - } + defaultProxyConfig(registry).getPropertyValues().add("proxyTargetClass", Boolean.TRUE); } public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { - if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + defaultProxyConfig(registry).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( @@ -115,12 +122,12 @@ public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry reg Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { - BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); - if (!cls.getName().equals(apcDefinition.getBeanClassName())) { - int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + BeanDefinition beanDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + if (!cls.getName().equals(beanDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(beanDefinition.getBeanClassName()); int requiredPriority = findPriorityForClass(cls); if (currentPriority < requiredPriority) { - apcDefinition.setBeanClassName(cls.getName()); + beanDefinition.setBeanClassName(cls.getName()); } } return null; @@ -128,8 +135,8 @@ public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry reg RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); beanDefinition.setSource(source); - beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); return beanDefinition; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java index 3b7b17052437..70f0c63122e6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java @@ -112,11 +112,13 @@ else if (advised.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE && if (isEligible(bean, beanName)) { ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName); - if (!proxyFactory.isProxyTargetClass()) { + if (!proxyFactory.isProxyTargetClass() && !proxyFactory.hasUserSuppliedInterfaces()) { evaluateProxyInterfaces(bean.getClass(), proxyFactory); } proxyFactory.addAdvisor(this.advisor); customizeProxyFactory(proxyFactory); + proxyFactory.setFrozen(isFrozen()); + proxyFactory.setPreFiltered(true); // Use original ClassLoader if bean class not locally loaded in overriding class loader ClassLoader classLoader = getProxyClassLoader(); @@ -187,6 +189,7 @@ protected boolean isEligible(Class targetClass) { protected ProxyFactory prepareProxyFactory(Object bean, String beanName) { ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); + proxyFactory.setFrozen(false); proxyFactory.setTarget(bean); return proxyFactory; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java index ba670ff33bd0..f91792694f06 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -70,6 +70,8 @@ public class AdvisedSupport extends ProxyConfig implements Advised { /** use serialVersionUID from Spring 2.0 for interoperability. */ private static final long serialVersionUID = 2651364800145442165L; + private static final Advisor[] EMPTY_ADVISOR_ARRAY = new Advisor[0]; + /** * Canonical TargetSource when there's no target, and behavior is @@ -288,7 +290,7 @@ private boolean isAdvisorIntroducedInterface(Class ifc) { @Override public final Advisor[] getAdvisors() { - return this.advisors.toArray(new Advisor[0]); + return this.advisors.toArray(EMPTY_ADVISOR_ARRAY); } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java index be021f99f58b..1efed81dec82 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -98,7 +98,7 @@ class CglibAopProxy implements AopProxy, Serializable { private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; - private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + private static final boolean COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent( "kotlinx.coroutines.reactor.MonoKt", CglibAopProxy.class.getClassLoader()); private static final GeneratorStrategy undeclaredThrowableStrategy = @@ -435,7 +435,7 @@ private static boolean implementsInterface(Method method, Set> ifcs) { throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } - if (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method)) { + if (COROUTINES_REACTOR_PRESENT && KotlinDetector.isSuspendingFunction(method)) { return COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()) ? CoroutinesUtils.asFlow(returnValue) : CoroutinesUtils.awaitSingleOrNull(returnValue, arguments[arguments.length - 1]); @@ -694,7 +694,7 @@ public DynamicAdvisedInterceptor(AdvisedSupport advised) { Object target = null; TargetSource targetSource = this.advised.getTargetSource(); try { - if (this.advised.exposeProxy) { + if (this.advised.isExposeProxy()) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java index 3cb74cbdf782..f0e56cab0fa1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java @@ -40,7 +40,7 @@ static Object asFlow(@Nullable Object publisher) { } } - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({"rawtypes", "unchecked"}) static @Nullable Object awaitSingleOrNull(@Nullable Object value, Object continuation) { return MonoKt.awaitSingleOrNull(value instanceof Mono mono ? mono : Mono.justOrEmpty(value), (Continuation) continuation); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java index a2b105839d27..b0016b00c039 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java @@ -75,7 +75,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow"; - private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + private static final boolean COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent( "kotlinx.coroutines.reactor.MonoKt", JdkDynamicAopProxy.class.getClassLoader()); /** We use a static Log to avoid serialization issues. */ @@ -183,7 +183,7 @@ else if (method.getDeclaringClass() == DecoratingProxy.class) { // There is only getDecoratedClass() declared -> dispatch to proxy config. 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)) { // Service invocations on ProxyConfig with the proxy config... return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); @@ -191,7 +191,7 @@ else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && Object retVal; - if (this.advised.exposeProxy) { + if (this.advised.isExposeProxy()) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; @@ -236,7 +236,7 @@ else if (retVal == null && returnType != void.class && returnType.isPrimitive()) throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } - if (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(method)) { + if (COROUTINES_REACTOR_PRESENT && KotlinDetector.isSuspendingFunction(method)) { return COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()) ? CoroutinesUtils.asFlow(retVal) : CoroutinesUtils.awaitSingleOrNull(retVal, args[args.length - 1]); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java index 0ff9bbc732f8..c516dac616ef 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java @@ -29,7 +29,7 @@ /** * Objenesis-based extension of {@link CglibAopProxy} to create proxy instances - * without invoking the constructor of the class. Used by default as of Spring 4. + * without invoking the constructor of the class. Used by default. * * @author Oliver Gierke * @author Juergen Hoeller diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java index 3c4ee97be346..ca21266ba0ec 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyConfig.java @@ -18,6 +18,8 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -34,15 +36,15 @@ public class ProxyConfig implements Serializable { 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 void setProxyTargetClass(boolean proxyTargetClass) { * Return whether to proxy the target class directly as well as any interfaces. */ public boolean isProxyTargetClass() { - return this.proxyTargetClass; + return (this.proxyTargetClass != null && this.proxyTargetClass); } /** @@ -85,7 +87,7 @@ public void setOptimize(boolean optimize) { * Return whether proxies should perform aggressive optimizations. */ public boolean isOptimize() { - return this.optimize; + return (this.optimize != null && this.optimize); } /** @@ -103,7 +105,7 @@ public void setOpaque(boolean opaque) { * prevented from being cast to {@link Advised}. */ public boolean isOpaque() { - return this.opaque; + return (this.opaque != null && this.opaque); } /** @@ -124,7 +126,7 @@ public void setExposeProxy(boolean exposeProxy) { * each invocation. */ public boolean isExposeProxy() { - return this.exposeProxy; + return (this.exposeProxy != null && this.exposeProxy); } /** @@ -141,7 +143,7 @@ public void setFrozen(boolean frozen) { * Return whether the config is frozen, and no advice changes can be made. */ public boolean isFrozen() { - return this.frozen; + return (this.frozen != null && this.frozen); } @@ -153,9 +155,34 @@ public void copyFrom(ProxyConfig other) { Assert.notNull(other, "Other ProxyConfig object must not be null"); this.proxyTargetClass = other.proxyTargetClass; this.optimize = other.optimize; + this.opaque = other.opaque; this.exposeProxy = other.exposeProxy; 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 diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java index 10a90ea8ee90..4c15b8995378 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/DefaultAdvisorAdapterRegistry.java @@ -40,6 +40,9 @@ @SuppressWarnings("serial") public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable { + private static final MethodInterceptor[] EMPTY_METHOD_INTERCEPTOR_ARRAY = new MethodInterceptor[0]; + + private final List adapters = new ArrayList<>(3); @@ -89,7 +92,7 @@ public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdvice if (interceptors.isEmpty()) { throw new UnknownAdviceTypeException(advisor.getAdvice()); } - return interceptors.toArray(new MethodInterceptor[0]); + return interceptors.toArray(EMPTY_METHOD_INTERCEPTOR_ARRAY); } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java index c8c2f56f2bd6..ccdf91877228 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java @@ -117,12 +117,6 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport /** Default is global AdvisorAdapterRegistry. */ 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. */ private String[] interceptorNames = new String[0]; @@ -141,22 +135,6 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport private final Map advisedBeans = new ConcurrentHashMap<>(256); - /** - * Set whether the proxy should be frozen, preventing advice - * from being added to it once it is created. - *

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. *

Default is the global {@link AdvisorAdapterRegistry}. @@ -206,6 +184,7 @@ public void setApplyCommonInterceptorsFirst(boolean applyCommonInterceptorsFirst @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; + AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory); } /** @@ -471,6 +450,24 @@ private Object buildProxy(Class beanClass, @Nullable String beanName, ProxyFactory proxyFactory = new ProxyFactory(); 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()) { // Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios) @@ -481,22 +478,13 @@ private Object buildProxy(Class beanClass, @Nullable String beanName, } } } - 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); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); - proxyFactory.setFrozen(this.freezeProxy); + proxyFactory.setFrozen(isFrozen()); if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java index c9d07a1fdf8d..256bdd5c9d56 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java @@ -25,9 +25,9 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; /** - * Extension of {@link AbstractAutoProxyCreator} which implements {@link BeanFactoryAware}, - * adds exposure of the original target class for each proxied bean - * ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}), + * Extension of {@link AbstractAdvisingBeanPostProcessor} which implements + * {@link BeanFactoryAware}, adds exposure of the original target class for each + * proxied bean ({@link AutoProxyUtils#ORIGINAL_TARGET_CLASS_ATTRIBUTE}), * and participates in an externally enforced target-class mode for any given bean * ({@link AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE}). * This post-processor is therefore aligned with {@link AbstractAutoProxyCreator}. @@ -47,6 +47,7 @@ public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends Abst @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = (beanFactory instanceof ConfigurableListableBeanFactory clbf ? clbf : null); + AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory); } @Override @@ -56,9 +57,19 @@ protected ProxyFactory prepareProxyFactory(Object bean, String beanName) { } ProxyFactory proxyFactory = super.prepareProxyFactory(bean, beanName); - if (!proxyFactory.isProxyTargetClass() && this.beanFactory != null && - AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) { - proxyFactory.setProxyTargetClass(true); + if (this.beanFactory != null) { + if (AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) { + 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; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java index b73b9abd5bbf..3522bfd8b668 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java @@ -18,6 +18,8 @@ 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.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -31,9 +33,37 @@ * @author Juergen Hoeller * @since 2.0.3 * @see AbstractAutoProxyCreator + * @see AbstractBeanFactoryAwareAdvisingPostProcessor */ 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 * 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"); + /** + * 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 * class rather than its interfaces. Checks the @@ -65,6 +136,7 @@ public abstract class AutoProxyUtils { * @param beanFactory the containing ConfigurableListableBeanFactory * @param beanName the name of the bean * @return whether the given bean should be proxied with its target class + * @see #PRESERVE_TARGET_CLASS_ATTRIBUTE */ public static boolean shouldProxyTargetClass( ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java index 002418c4fba1..426c37dd77a5 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreator.java @@ -57,9 +57,8 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator { * A name can specify a prefix to match by ending with "*", for example, "myBean,tx*" * will match the bean named "myBean" and all beans whose name start with "tx". *

NOTE: In case of a FactoryBean, only the objects created by the - * FactoryBean will get proxied. This default behavior applies as of Spring 2.0. - * If you intend to proxy a FactoryBean instance itself (a rare use case, but - * Spring 1.2's default behavior), specify the bean name of the FactoryBean + * FactoryBean will get proxied. If you intend to proxy a FactoryBean instance + * itself (a rare use case), specify the bean name of the FactoryBean * including the factory-bean prefix "&": for example, "&myFactoryBean". * @see org.springframework.beans.factory.FactoryBean * @see org.springframework.beans.factory.BeanFactory#FACTORY_BEAN_PREFIX diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java index d77e4377c631..32f68f2c7b1a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java @@ -82,13 +82,19 @@ public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder defini // Copy autowire settings from original bean definition. proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); + proxyDefinition.setFallback(targetDefinition.isFallback()); if (targetDefinition instanceof AbstractBeanDefinition abd) { + proxyDefinition.setDefaultCandidate(abd.isDefaultCandidate()); proxyDefinition.copyQualifiersFrom(abd); } // The target bean should be ignored in favor of the scoped proxy. targetDefinition.setAutowireCandidate(false); targetDefinition.setPrimary(false); + targetDefinition.setFallback(false); + if (targetDefinition instanceof AbstractBeanDefinition abd) { + abd.setDefaultCandidate(false); + } // Register the target bean as separate bean in the factory. registry.registerBeanDefinition(targetBeanName, targetDefinition); diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java index 1aceba918bcc..9138d1c32a39 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java @@ -66,7 +66,7 @@ */ public abstract class AopUtils { - private static final boolean coroutinesReactorPresent = ClassUtils.isPresent( + private static final boolean COROUTINES_REACTOR_PRESENT = ClassUtils.isPresent( "kotlinx.coroutines.reactor.MonoKt", AopUtils.class.getClassLoader()); @@ -355,7 +355,7 @@ public static List findAdvisorsThatCanApply(List candidateAdvi try { Method originalMethod = BridgeMethodResolver.findBridgedMethod(method); ReflectionUtils.makeAccessible(originalMethod); - return (coroutinesReactorPresent && KotlinDetector.isSuspendingFunction(originalMethod) ? + return (COROUTINES_REACTOR_PRESENT && KotlinDetector.isSuspendingFunction(originalMethod) ? KotlinDelegate.invokeSuspendingFunction(originalMethod, target, args) : originalMethod.invoke(target, args)); } catch (InvocationTargetException ex) { diff --git a/spring-aop/src/main/java/org/springframework/aop/target/CommonsPool2TargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/CommonsPool2TargetSource.java index 9db40a729542..6654b080f9bf 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/CommonsPool2TargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/CommonsPool2TargetSource.java @@ -46,8 +46,6 @@ * meaningful validation. All exposed Commons Pool properties use the * corresponding Commons Pool defaults. * - *

Compatible with Apache Commons Pool 2.4, as of Spring 4.2. - * * @author Rod Johnson * @author Rob Harrop * @author Juergen Hoeller @@ -63,7 +61,7 @@ * @see #setTimeBetweenEvictionRunsMillis * @see #setMinEvictableIdleTimeMillis */ -@SuppressWarnings({"rawtypes", "unchecked", "serial"}) +@SuppressWarnings({"rawtypes", "unchecked", "serial", "deprecation"}) public class CommonsPool2TargetSource extends AbstractPoolingTargetSource implements PooledObjectFactory { private int maxIdle = GenericObjectPoolConfig.DEFAULT_MAX_IDLE; diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java index 70b347282778..ec751f46e33c 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java @@ -37,7 +37,6 @@ * Tests for {@link AsyncExecutionInterceptor}. * * @author Bao Ngo - * @since 7.0 */ class AsyncExecutionInterceptorTests { @@ -62,11 +61,13 @@ interface GenericRunner { O run(); } + static class FutureRunner implements GenericRunner> { + @Override public Future run() { - return CompletableFuture.runAsync(() -> { - }); + return CompletableFuture.runAsync(() -> {}); } } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java index ff3299cd4a50..de5e76db2b71 100644 --- a/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/scope/ScopedProxyUtilsTests.java @@ -18,6 +18,15 @@ import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.AutowireCandidateQualifier; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -25,6 +34,7 @@ * Tests for {@link ScopedProxyUtils}. * * @author Sam Brannen + * @author Juergen Hoeller * @since 5.1.10 */ class ScopedProxyUtilsTests { @@ -53,15 +63,79 @@ void getOriginalBeanNameAndIsScopedTarget() { @Test void getOriginalBeanNameForNullTargetBean() { assertThatIllegalArgumentException() - .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null)) - .withMessage("bean name 'null' does not refer to the target of a scoped proxy"); + .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName(null)) + .withMessage("bean name 'null' does not refer to the target of a scoped proxy"); } @Test void getOriginalBeanNameForNonScopedTarget() { assertThatIllegalArgumentException() - .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean")) - .withMessage("bean name 'myBean' does not refer to the target of a scoped proxy"); + .isThrownBy(() -> ScopedProxyUtils.getOriginalBeanName("myBean")) + .withMessage("bean name 'myBean' does not refer to the target of a scoped proxy"); + } + + @Test + void createScopedProxyTargetAppliesAutowireSettingsToProxyBeanDefinition() { + AbstractBeanDefinition targetDefinition = new GenericBeanDefinition(); + // Opposite of defaults + targetDefinition.setAutowireCandidate(false); + targetDefinition.setDefaultCandidate(false); + targetDefinition.setPrimary(true); + targetDefinition.setFallback(true); + + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy( + new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false); + AbstractBeanDefinition proxyBeanDefinition = (AbstractBeanDefinition) proxyHolder.getBeanDefinition(); + + assertThat(proxyBeanDefinition.isAutowireCandidate()).isFalse(); + assertThat(proxyBeanDefinition.isDefaultCandidate()).isFalse(); + assertThat(proxyBeanDefinition.isPrimary()).isTrue(); + assertThat(proxyBeanDefinition.isFallback()).isTrue(); + } + + @Test + void createScopedProxyTargetAppliesBeanAttributesToProxyBeanDefinition() { + GenericBeanDefinition targetDefinition = new GenericBeanDefinition(); + // Opposite of defaults + targetDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + targetDefinition.setSource("theSource"); + targetDefinition.addQualifier(new AutowireCandidateQualifier("myQualifier")); + + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy( + new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false); + BeanDefinition proxyBeanDefinition = proxyHolder.getBeanDefinition(); + + assertThat(proxyBeanDefinition.getRole()).isEqualTo(BeanDefinition.ROLE_INFRASTRUCTURE); + assertThat(proxyBeanDefinition).isInstanceOf(RootBeanDefinition.class); + assertThat(proxyBeanDefinition.getPropertyValues()).hasSize(2); + assertThat(proxyBeanDefinition.getPropertyValues().get("proxyTargetClass")).isEqualTo(false); + assertThat(proxyBeanDefinition.getPropertyValues().get("targetBeanName")).isEqualTo( + ScopedProxyUtils.getTargetBeanName("myBean")); + + RootBeanDefinition rootBeanDefinition = (RootBeanDefinition) proxyBeanDefinition; + assertThat(rootBeanDefinition.getQualifiers()).hasSize(1); + assertThat(rootBeanDefinition.hasQualifier("myQualifier")).isTrue(); + assertThat(rootBeanDefinition.getSource()).isEqualTo("theSource"); + } + + @Test + void createScopedProxyTargetCleansAutowireSettingsInTargetDefinition() { + AbstractBeanDefinition targetDefinition = new GenericBeanDefinition(); + targetDefinition.setAutowireCandidate(true); + targetDefinition.setDefaultCandidate(true); + targetDefinition.setPrimary(true); + targetDefinition.setFallback(true); + + BeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry(); + ScopedProxyUtils.createScopedProxy( + new BeanDefinitionHolder(targetDefinition, "myBean"), registry, false); + + assertThat(targetDefinition.isAutowireCandidate()).isFalse(); + assertThat(targetDefinition.isDefaultCandidate()).isFalse(); + assertThat(targetDefinition.isPrimary()).isFalse(); + assertThat(targetDefinition.isFallback()).isFalse(); } } diff --git a/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt index 10e632abd396..0489d68bb0c3 100644 --- a/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt +++ b/spring-aop/src/test/kotlin/org/springframework/aop/framework/CglibAopProxyKotlinTests.kt @@ -19,6 +19,7 @@ package org.springframework.aop.framework import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.Test +import java.time.LocalDateTime /** * Tests for Kotlin support in [CglibAopProxy]. @@ -48,6 +49,13 @@ class CglibAopProxyKotlinTests { assertThatThrownBy { proxy.checkedException() }.isInstanceOf(CheckedException::class.java) } + @Test // gh-35487 + fun jvmDefault() { + val proxyFactory = ProxyFactory() + proxyFactory.setTarget(AddressRepo()) + proxyFactory.proxy + } + open class MyKotlinBean { @@ -63,4 +71,24 @@ class CglibAopProxyKotlinTests { } class CheckedException() : Exception() + + open class AddressRepo(): CrudRepo + + interface CrudRepo { + fun save(e: E): E { + return e + } + fun delete(id: ID): Long { + return 0L + } + } + + data class Address( + val id: Int = 0, + val street: String, + val version: Int = 0, + val createdAt: LocalDateTime? = null, + val updatedAt: LocalDateTime? = null, + ) + } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java index 5d8d01eef33e..495acb6710c4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java @@ -88,6 +88,8 @@ public abstract class BeanUtils { double.class, 0D, char.class, '\0'); + private static final boolean KOTLIN_REFLECT_PRESENT = KotlinDetector.isKotlinReflectPresent(); + /** * Convenience method to instantiate a class using its no-arg constructor. @@ -186,7 +188,7 @@ public static T instantiateClass(Constructor ctor, @Nullable Object... ar Assert.notNull(ctor, "Constructor must not be null"); try { ReflectionUtils.makeAccessible(ctor); - if (KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { + if (KOTLIN_REFLECT_PRESENT && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { return KotlinDelegate.instantiateClass(ctor, args); } else { @@ -279,7 +281,7 @@ else if (ctors.length == 0) { */ public static @Nullable Constructor findPrimaryConstructor(Class clazz) { Assert.notNull(clazz, "Class must not be null"); - if (KotlinDetector.isKotlinType(clazz)) { + if (KOTLIN_REFLECT_PRESENT && KotlinDetector.isKotlinType(clazz)) { return KotlinDelegate.findPrimaryConstructor(clazz); } if (clazz.isRecord()) { @@ -659,7 +661,7 @@ public static MethodParameter getWriteMethodParameter(PropertyDescriptor pd) { ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); @Nullable String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); - int parameterCount = (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasDefaultConstructorMarker(ctor) ? + int parameterCount = (KOTLIN_REFLECT_PRESENT && KotlinDelegate.hasDefaultConstructorMarker(ctor) ? ctor.getParameterCount() - 1 : ctor.getParameterCount()); Assert.state(paramNames.length == parameterCount, () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java index 8532d26e40ee..2a80f47584a2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java @@ -20,8 +20,6 @@ import java.beans.IntrospectionException; import java.lang.reflect.Method; -import org.jspecify.annotations.NonNull; - import org.springframework.core.Ordered; /** @@ -44,7 +42,7 @@ public class ExtendedBeanInfoFactory extends StandardBeanInfoFactory { @Override - public @NonNull BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { + public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { BeanInfo beanInfo = super.getBeanInfo(beanClass); return (supports(beanClass) ? new ExtendedBeanInfo(beanInfo) : beanInfo); } diff --git a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java index 1ffc04afa5e7..f78e16af92a7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java @@ -44,6 +44,9 @@ @SuppressWarnings("serial") public class MutablePropertyValues implements PropertyValues, Serializable { + private static final PropertyValue[] EMPTY_PROPERTY_VALUE_ARRAY = new PropertyValue[0]; + + private final List propertyValueList; private @Nullable Set processedProperties; @@ -264,7 +267,7 @@ public Stream stream() { @Override public PropertyValue[] getPropertyValues() { - return this.propertyValueList.toArray(new PropertyValue[0]); + return this.propertyValueList.toArray(EMPTY_PROPERTY_VALUE_ARRAY); } @Override diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java index c1b309154da5..ad47efb2b2ae 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java @@ -19,6 +19,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; /** @@ -264,6 +265,22 @@ public interface BeanFactory { */ ObjectProvider getBeanProvider(ResolvableType requiredType); + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. This variant allows + * for specifying a generic type to match, similar to reflective injection points + * with generic type declarations in method/constructor parameters. + *

This is a variant of {@link #getBeanProvider(ResolvableType)} with a + * captured generic type for type-safe retrieval, typically used inline: + * {@code getBeanProvider(new ParameterizedTypeReference<>() {})} - and + * effectively equivalent to {@code getBeanProvider(ResolvableType.forType(...))}. + * @return a corresponding provider handle + * @param requiredType a captured generic type that the bean must match + * @since 7.0 + * @see #getBeanProvider(ResolvableType) + */ + ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType); + /** * Does this bean factory contain a bean definition or externally registered singleton * instance with the given name? diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java index a9beddaf545a..3348da86fd8e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java @@ -24,7 +24,6 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.env.Environment; @@ -55,43 +54,92 @@ public interface BeanRegistry { void registerAlias(String name, String alias); /** - * Register a bean from the given bean class, which will be instantiated using the + * Register a bean from the given class, which will be instantiated using the * related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. + *

For registering a bean with a generic type, consider + * {@link #registerBean(ParameterizedTypeReference)}. * @param beanClass the class of the bean * @return the generated bean name + * @see #registerBean(Class) */ String registerBean(Class beanClass); /** - * Register a bean from the given bean class, customizing it with the customizer + * Register a bean from the given generics-containing type, which will be + * instantiated using the related + * {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. + * @param beanType the generics-containing type of the bean + * @return the generated bean name + */ + String registerBean(ParameterizedTypeReference beanType); + + /** + * Register a bean from the given class, customizing it with the customizer * callback. The bean will be instantiated using the supplier that can be configured * in the customizer callback, or will be tentatively instantiated with its * {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise. + *

For registering a bean with a generic type, consider + * {@link #registerBean(ParameterizedTypeReference, Consumer)}. * @param beanClass the class of the bean - * @param customizer callback to customize other bean properties than the name + * @param customizer the callback to customize other bean properties than the name * @return the generated bean name */ String registerBean(Class beanClass, Consumer> customizer); /** - * Register a bean from the given bean class, which will be instantiated using the + * Register a bean from the given generics-containing type, customizing it + * with the customizer callback. The bean will be instantiated using the supplier + * that can be configured in the customizer callback, or will be tentatively instantiated + * with its {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise. + * @param beanType the generics-containing type of the bean + * @param customizer the callback to customize other bean properties than the name + * @return the generated bean name + */ + String registerBean(ParameterizedTypeReference beanType, Consumer> customizer); + + /** + * Register a bean from the given class, which will be instantiated using the * related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. + *

For registering a bean with a generic type, consider + * {@link #registerBean(String, ParameterizedTypeReference)}. * @param name the name of the bean * @param beanClass the class of the bean */ void registerBean(String name, Class beanClass); /** - * Register a bean from the given bean class, customizing it with the customizer + * Register a bean from the given generics-containing type, which + * will be instantiated using the related + * {@link BeanUtils#getResolvableConstructor resolvable constructor} if any. + * @param name the name of the bean + * @param beanType the generics-containing type of the bean + */ + void registerBean(String name, ParameterizedTypeReference beanType); + + /** + * Register a bean from the given class, customizing it with the customizer * callback. The bean will be instantiated using the supplier that can be configured * in the customizer callback, or will be tentatively instantiated with its * {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise. + *

For registering a bean with a generic type, consider + * {@link #registerBean(String, ParameterizedTypeReference, Consumer)}. * @param name the name of the bean * @param beanClass the class of the bean - * @param customizer callback to customize other bean properties than the name + * @param customizer the callback to customize other bean properties than the name */ void registerBean(String name, Class beanClass, Consumer> customizer); + /** + * Register a bean from the given generics-containing type, customizing it + * with the customizer callback. The bean will be instantiated using the supplier + * that can be configured in the customizer callback, or will be tentatively instantiated + * with its {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise. + * @param name the name of the bean + * @param beanType the generics-containing type of the bean + * @param customizer the callback to customize other bean properties than the name + */ + void registerBean(String name, ParameterizedTypeReference beanType, Consumer> customizer); + /** * Specification for customizing a bean. @@ -164,20 +212,6 @@ interface Spec { * @see AbstractBeanDefinition#setInstanceSupplier(Supplier) */ Spec supplier(Function supplier); - - /** - * Set a generics-containing target type of this bean. - * @see #targetType(ResolvableType) - * @see RootBeanDefinition#setTargetType(ResolvableType) - */ - Spec targetType(ParameterizedTypeReference type); - - /** - * Set a generics-containing target type of this bean. - * @see #targetType(ParameterizedTypeReference) - * @see RootBeanDefinition#setTargetType(ResolvableType) - */ - Spec targetType(ResolvableType type); } @@ -188,32 +222,40 @@ interface Spec { interface SupplierContext { /** - * Return the bean instance that uniquely matches the given object type, if any. - * @param requiredType type the bean must match; can be an interface or superclass - * @return an instance of the single bean matching the required type + * Return the bean instance that uniquely matches the given type, if any. + * @param beanClass the type the bean must match; can be an interface or superclass + * @return an instance of the single bean matching the bean type + * @see BeanFactory#getBean(String) + */ + T bean(Class beanClass) throws BeansException; + + /** + * Return the bean instance that uniquely matches the given generics-containing type, if any. + * @param beanType the generics-containing type the bean must match; can be an interface or superclass + * @return an instance of the single bean matching the bean type * @see BeanFactory#getBean(String) */ - T bean(Class requiredType) throws BeansException; + T bean(ParameterizedTypeReference beanType) throws BeansException; /** * Return an instance, which may be shared or independent, of the * specified bean. * @param name the name of the bean to retrieve - * @param requiredType type the bean must match; can be an interface or superclass + * @param beanClass the type the bean must match; can be an interface or superclass * @return an instance of the bean. * @see BeanFactory#getBean(String, Class) */ - T bean(String name, Class requiredType) throws BeansException; + T bean(String name, Class beanClass) throws BeansException; /** * Return a provider for the specified bean, allowing for lazy on-demand retrieval * of instances, including availability and uniqueness options. - *

For matching a generic type, consider {@link #beanProvider(ResolvableType)}. - * @param requiredType type the bean must match; can be an interface or superclass + *

For matching a generic type, consider {@link #beanProvider(ParameterizedTypeReference)}. + * @param beanClass the type the bean must match; can be an interface or superclass * @return a corresponding provider handle * @see BeanFactory#getBeanProvider(Class) */ - ObjectProvider beanProvider(Class requiredType); + ObjectProvider beanProvider(Class beanClass); /** * Return a provider for the specified bean, allowing for lazy on-demand retrieval @@ -229,11 +271,11 @@ interface SupplierContext { * Java compiler warning), consider calling {@link #beanProvider(Class)} with the * raw type as a second step if no full generic match is * {@link ObjectProvider#getIfAvailable() available} with this variant. - * @param requiredType type the bean must match; can be a generic type declaration + * @param beanType the generics-containing type the bean must match; can be an interface or superclass * @return a corresponding provider handle * @see BeanFactory#getBeanProvider(ResolvableType) */ - ObjectProvider beanProvider(ResolvableType requiredType); + ObjectProvider beanProvider(ParameterizedTypeReference beanType); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java index 97250ad91fb3..0da024b6ae23 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/SmartFactoryBean.java @@ -31,8 +31,11 @@ *

As of 7.0, this interface also allows for exposing additional object * types for dependency injection through implementing a pair of methods: * {@link #getObject(Class)} as well as {@link #supportsType(Class)}. - * The primary {@link #getObjectType()} will be exposed for regular access. - * Only if a specific type is requested, additional types are considered. + * The primary {@link #getObjectType()} will be exposed for regular access; + * only if a specific type is requested, additional types are considered. + * The container will not cache {@code SmartFactoryBean}-produced objects; + * make sure that the {@code getObject} implementation is thread-safe for + * repeated invocations. * *

NOTE: This interface is a special purpose interface, mainly for * internal use within the framework and within collaborating frameworks. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index 60de165d3317..8d97f4e3dad4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java @@ -549,7 +549,7 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { } final List elements = new ArrayList<>(); - Class targetClass = clazz; + Class targetClass = ClassUtils.getUserClass(clazz); do { final List fieldElements = new ArrayList<>(); @@ -569,12 +569,11 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { final List methodElements = new ArrayList<>(); ReflectionUtils.doWithLocalMethods(targetClass, method -> { - Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); - if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { + if (method.isBridge()) { return; } - MergedAnnotation ann = findAutowiredAnnotation(bridgedMethod); - if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + MergedAnnotation ann = findAutowiredAnnotation(method); + if (ann != null && method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { if (logger.isInfoEnabled()) { logger.info("Autowired annotation is not supported on static methods: " + method); @@ -592,7 +591,7 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { } } boolean required = determineRequiredStatus(ann); - PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz); methodElements.add(new AutowiredMethodElement(method, required, pd)); } }); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java index f8104418e88f..4f4dc58622d6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java @@ -45,6 +45,8 @@ */ public final class ParameterResolutionDelegate { + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + private static final AnnotatedElement EMPTY_ANNOTATED_ELEMENT = new AnnotatedElement() { @Override public @Nullable T getAnnotation(Class annotationClass) { @@ -52,11 +54,11 @@ public final class ParameterResolutionDelegate { } @Override public Annotation[] getAnnotations() { - return new Annotation[0]; + return EMPTY_ANNOTATION_ARRAY; } @Override public Annotation[] getDeclaredAnnotations() { - return new Annotation[0]; + return EMPTY_ANNOTATION_ARRAY; } }; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index ad99cee7d8f5..38f0bbed24bb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -172,7 +172,6 @@ public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDesc * {@code true} if a qualifier has been found and matched, * {@code null} if no qualifier has been found at all */ - protected @Nullable Boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) { boolean qualifierFound = false; if (!ObjectUtils.isEmpty(annotationsToSearch)) { @@ -364,6 +363,14 @@ public boolean hasQualifier(DependencyDescriptor descriptor) { return true; } } + MethodParameter methodParam = descriptor.getMethodParameter(); + if (methodParam != null) { + for (Annotation annotation : methodParam.getMethodAnnotations()) { + if (isQualifier(annotation.annotationType())) { + return true; + } + } + } return false; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java index d52d0a033664..6fc2e7c093bb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegates.java @@ -32,6 +32,7 @@ import org.springframework.aot.generate.ValueCodeGeneratorDelegates; import org.springframework.aot.generate.ValueCodeGeneratorDelegates.CollectionDelegate; import org.springframework.aot.generate.ValueCodeGeneratorDelegates.MapDelegate; +import org.springframework.beans.factory.config.AutowiredPropertyMarker; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.TypedStringValue; @@ -59,6 +60,7 @@ public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { *

  • {@link LinkedHashMap}
  • *
  • {@link BeanReference}
  • *
  • {@link TypedStringValue}
  • + *
  • {@link AutowiredPropertyMarker}
  • * * When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the * delegates for common value types}, this should be added first as they have @@ -70,7 +72,8 @@ public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { new ManagedMapDelegate(), new LinkedHashMapDelegate(), new BeanReferenceDelegate(), - new TypedStringValueDelegate() + new TypedStringValueDelegate(), + new AutowiredPropertyMarkerDelegate() ); @@ -234,4 +237,19 @@ private CodeBlock generateTypeStringValueCode(ValueCodeGenerator valueCodeGenera return valueCodeGenerator.generateCode(value); } } + + /** + * {@link Delegate} for {@link AutowiredPropertyMarker} types. + */ + private static class AutowiredPropertyMarkerDelegate implements Delegate { + + @Override + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + if (value instanceof AutowiredPropertyMarker) { + return CodeBlock.of("$T.INSTANCE", AutowiredPropertyMarker.class); + } + return null; + } + } + } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java index 10489b71b3aa..ff978aabc45f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java @@ -88,6 +88,8 @@ public class InstanceSupplierCodeGenerator { private static final CodeBlock NO_ARGS = CodeBlock.of(""); + private static final boolean KOTLIN_REFLECT_PRESENT = KotlinDetector.isKotlinReflectPresent(); + private final GenerationContext generationContext; @@ -161,7 +163,7 @@ private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Cons registeredBean.getBeanName(), constructor, registeredBean.getBeanClass()); Class publicType = descriptor.publicType(); - if (KotlinDetector.isKotlinType(publicType) && KotlinDelegate.hasConstructorWithOptionalParameter(publicType)) { + if (KOTLIN_REFLECT_PRESENT && KotlinDetector.isKotlinType(publicType) && KotlinDelegate.hasConstructorWithOptionalParameter(publicType)) { return generateCodeForInaccessibleConstructor(descriptor, hints -> hints.registerType(publicType, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); } @@ -293,9 +295,12 @@ private CodeBlock generateCodeForInaccessibleFactoryMethod( this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INVOKE); GeneratedMethod getInstanceMethod = generateGetInstanceSupplierMethod(method -> { + CodeWarnings codeWarnings = new CodeWarnings(); Class suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType()); + codeWarnings.detectDeprecation(suppliedType, factoryMethod); method.addJavadoc("Get the bean instance supplier for '$L'.", beanName); method.addModifiers(PRIVATE_STATIC); + codeWarnings.suppress(method); method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, suppliedType)); method.addStatement(generateInstanceSupplierForFactoryMethod( factoryMethod, suppliedType, targetClass, factoryMethod.getName())); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java index 098ca8dd802a..36ac6c02b606 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java @@ -81,7 +81,7 @@ public interface BeanPostProcessor { * or a custom init-method). The bean will already be populated with property values. * The returned bean instance may be a wrapper around the original. *

    In case of a FactoryBean, this callback will be invoked for both the FactoryBean - * instance and the objects created by the FactoryBean (as of Spring 2.0). The + * instance and the objects created by the FactoryBean. The * post-processor can decide whether to apply to either the FactoryBean or created * objects or both through corresponding {@code bean instanceof FactoryBean} checks. *

    This callback will also be invoked after a short-circuiting triggered by a diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java index 92aa59c2b069..9df8898f156d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java @@ -153,6 +153,18 @@ boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) */ boolean isConfigurationFrozen(); + /** + * Mark current thread as main bootstrap thread for singleton instantiation, + * with lenient bootstrap locking applying for background threads. + *

    Any such marker is to be removed at the end of the managed bootstrap in + * {@link #preInstantiateSingletons()}. + * @since 6.2.12 + * @see #setBootstrapExecutor + * @see #preInstantiateSingletons() + */ + default void prepareSingletonBootstrap() { + } + /** * Ensure that all non-lazy-init singletons are instantiated, also considering * {@link org.springframework.beans.factory.FactoryBean FactoryBeans}. @@ -160,6 +172,7 @@ boolean isAutowireCandidate(String beanName, DependencyDescriptor descriptor) * @throws BeansException if one of the singleton beans could not be created. * Note: This may have left the factory with some beans already initialized! * Call {@link #destroySingletons()} for full cleanup in this case. + * @see #prepareSingletonBootstrap() * @see #destroySingletons() */ void preInstantiateSingletons() throws BeansException; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java index a156999802d9..f53577834ac9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java @@ -33,7 +33,7 @@ * registration of custom {@link PropertyEditor property editors}. * *

    In case you want to register {@link PropertyEditor} instances, - * the recommended usage as of Spring 2.0 is to use custom + * the recommended usage is to use custom * {@link PropertyEditorRegistrar} implementations that in turn register any * desired editor instances on a given * {@link org.springframework.beans.PropertyEditorRegistry registry}. Each diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java index 882290de403b..f25ce93707e7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java @@ -65,7 +65,7 @@ * Note that the value of "foo" in the first document is not simply replaced * with the value in the second, but its nested values are merged. * - *

    Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. + *

    Requires SnakeYAML 2.0 or higher. * * @author Dave Syer * @author Juergen Hoeller diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java index 99c0d87ef9dc..13648d824aad 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -50,7 +50,7 @@ /** * Base class for YAML factories. * - *

    Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. + *

    Requires SnakeYAML 2.0 or higher. * * @author Dave Syer * @author Juergen Hoeller diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java index a0a4107c84f7..8eb30cce9da0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java @@ -75,7 +75,7 @@ * servers[1]=foo.bar.com * * - *

    Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1. + *

    Requires SnakeYAML 2.0 or higher. * * @author Dave Syer * @author Stephane Nicoll diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java index 79dee8499c36..6deb0da0efe4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java @@ -37,6 +37,11 @@ */ public class BeanComponentDefinition extends BeanDefinitionHolder implements ComponentDefinition { + private static final BeanDefinition[] EMPTY_BEAN_DEFINITION_ARRAY = new BeanDefinition[0]; + + private static final BeanReference[] EMPTY_BEAN_REFERENCE_ARRAY = new BeanReference[0]; + + private final BeanDefinition[] innerBeanDefinitions; private final BeanReference[] beanReferences; @@ -84,8 +89,8 @@ else if (value instanceof BeanReference beanRef) { references.add(beanRef); } } - this.innerBeanDefinitions = innerBeans.toArray(new BeanDefinition[0]); - this.beanReferences = references.toArray(new BeanReference[0]); + this.innerBeanDefinitions = innerBeans.toArray(EMPTY_BEAN_DEFINITION_ARRAY); + this.beanReferences = references.toArray(EMPTY_BEAN_REFERENCE_ARRAY); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 20510109a408..e010d1483f65 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -254,9 +254,8 @@ public boolean isAllowCircularReferences() { *

    This will only be used as a last resort in case of a circular reference * that cannot be resolved otherwise: essentially, preferring a raw instance * getting injected over a failure of the entire bean wiring process. - *

    Default is "false", as of Spring 2.0. Turn this on to allow for non-wrapped - * raw beans injected into some of your references, which was Spring 1.2's - * (arguably unclean) default behavior. + *

    Default is "false". Turn this on to allow for non-wrapped + * raw beans injected into some of your references. *

    NOTE: It is generally recommended to not rely on circular references * between your beans, in particular with auto-proxying involved. * @see #setAllowCircularReferences diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 9f598f2c0001..d44f96017e57 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -514,8 +514,7 @@ public boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuc * 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. * @param name the name of the bean to query - * @param typeToMatch the type to match against (as a - * {@code ResolvableType}) + * @param typeToMatch the type to match against (as a {@code ResolvableType}) * @return {@code true} if the bean type matches, {@code false} if it * doesn't match or cannot be determined yet * @throws NoSuchBeanDefinitionException if there is no bean with the given name diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java index 9131786e5311..103c59dd2f5f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.lang.reflect.Constructor; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; @@ -92,6 +93,14 @@ public String registerBean(Class beanClass) { return beanName; } + @Override + public String registerBean(ParameterizedTypeReference beanType) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + String beanName = BeanDefinitionReaderUtils.uniqueBeanName(Objects.requireNonNull(resolvableType.resolve()).getName(), this.beanRegistry); + registerBean(beanName, beanType); + return beanName; + } + @Override public String registerBean(Class beanClass, Consumer> customizer) { String beanName = BeanDefinitionReaderUtils.uniqueBeanName(beanClass.getName(), this.beanRegistry); @@ -99,6 +108,15 @@ public String registerBean(Class beanClass, Consumer> customizer) return beanName; } + @Override + public String registerBean(ParameterizedTypeReference beanType, Consumer> customizer) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + Class beanClass = Objects.requireNonNull(resolvableType.resolve()); + String beanName = BeanDefinitionReaderUtils.uniqueBeanName(beanClass.getName(), this.beanRegistry); + registerBean(beanName, beanType, customizer); + return beanName; + } + @Override public void registerBean(String name, Class beanClass) { BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); @@ -111,9 +129,11 @@ public void registerBean(String name, Class beanClass) { } @Override - public void registerBean(String name, Class beanClass, Consumer> spec) { + public void registerBean(String name, ParameterizedTypeReference beanType) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + Class beanClass = Objects.requireNonNull(resolvableType.resolve()); BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); - spec.accept(new BeanSpecAdapter<>(beanDefinition, this.beanFactory)); + beanDefinition.setTargetType(resolvableType); if (this.customizers != null && this.customizers.containsKey(name)) { for (BeanDefinitionCustomizer customizer : this.customizers.get(name)) { customizer.customize(beanDefinition); @@ -122,6 +142,33 @@ public void registerBean(String name, Class beanClass, Consumer> this.beanRegistry.registerBeanDefinition(name, beanDefinition); } + @Override + public void registerBean(String name, Class beanClass, Consumer> customizer) { + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + customizer.accept(new BeanSpecAdapter<>(beanDefinition, this.beanFactory)); + if (this.customizers != null && this.customizers.containsKey(name)) { + for (BeanDefinitionCustomizer registryCustomizer : this.customizers.get(name)) { + registryCustomizer.customize(beanDefinition); + } + } + this.beanRegistry.registerBeanDefinition(name, beanDefinition); + } + + @Override + public void registerBean(String name, ParameterizedTypeReference beanType, Consumer> customizer) { + ResolvableType resolvableType = ResolvableType.forType(beanType); + Class beanClass = Objects.requireNonNull(resolvableType.resolve()); + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + beanDefinition.setTargetType(resolvableType); + customizer.accept(new BeanSpecAdapter<>(beanDefinition, this.beanFactory)); + if (this.customizers != null && this.customizers.containsKey(name)) { + for (BeanDefinitionCustomizer registryCustomizer : this.customizers.get(name)) { + registryCustomizer.customize(beanDefinition); + } + } + this.beanRegistry.registerBeanDefinition(name, beanDefinition); + } + @Override public void register(BeanRegistrar registrar) { Assert.notNull(registrar, "'registrar' must not be null"); @@ -238,18 +285,6 @@ public Spec supplier(Function supplier) { supplier.apply(new SupplierContextAdapter(this.beanFactory))); return this; } - - @Override - public Spec targetType(ParameterizedTypeReference targetType) { - this.beanDefinition.setTargetType(ResolvableType.forType(targetType)); - return this; - } - - @Override - public Spec targetType(ResolvableType targetType) { - this.beanDefinition.setTargetType(targetType); - return this; - } } @@ -262,23 +297,28 @@ public SupplierContextAdapter(ListableBeanFactory beanFactory) { } @Override - public T bean(Class requiredType) throws BeansException { - return this.beanFactory.getBean(requiredType); + public T bean(Class beanClass) throws BeansException { + return this.beanFactory.getBean(beanClass); + } + + @Override + public T bean(ParameterizedTypeReference beanType) throws BeansException { + return this.beanFactory.getBeanProvider(beanType).getObject(); } @Override - public T bean(String name, Class requiredType) throws BeansException { - return this.beanFactory.getBean(name, requiredType); + public T bean(String name, Class beanClass) throws BeansException { + return this.beanFactory.getBean(name, beanClass); } @Override - public ObjectProvider beanProvider(Class requiredType) { - return this.beanFactory.getBeanProvider(requiredType); + public ObjectProvider beanProvider(Class beanClass) { + return this.beanFactory.getBeanProvider(beanClass); } @Override - public ObjectProvider beanProvider(ResolvableType requiredType) { - return this.beanFactory.getBeanProvider(requiredType); + public ObjectProvider beanProvider(ParameterizedTypeReference beanType) { + return this.beanFactory.getBeanProvider(beanType); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 316220dfdc64..1941d300ccaf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -60,6 +60,7 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanNotOfRequiredTypeException; import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; @@ -77,6 +78,7 @@ import org.springframework.core.NamedThreadLocal; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.SpringProperties; import org.springframework.core.annotation.MergedAnnotation; @@ -191,8 +193,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto /** Map from bean name to merged BeanDefinitionHolder. */ private final Map mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256); - /** Set of bean definition names with a primary marker. */ - private final Set primaryBeanNames = ConcurrentHashMap.newKeySet(16); + /** Map of bean definition names with a primary marker plus corresponding type. */ + private final Map> primaryBeanNamesWithType = new ConcurrentHashMap<>(16); /** Map of singleton and non-singleton bean names, keyed by dependency type. */ private final Map, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64); @@ -397,6 +399,10 @@ public ObjectProvider getBeanProvider(ResolvableType requiredType) { return getBeanProvider(requiredType, true); } + public ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType) { + return getBeanProvider(ResolvableType.forType(requiredType), true); + } + //--------------------------------------------------------------------- // Implementation of ListableBeanFactory interface @@ -1024,7 +1030,7 @@ protected boolean isBeanEligibleForMetadataCaching(String beanName) { protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) { super.cacheMergedBeanDefinition(mbd, beanName); if (mbd.isPrimary()) { - this.primaryBeanNames.add(beanName); + this.primaryBeanNamesWithType.put(beanName, Void.class); } } @@ -1087,6 +1093,11 @@ else if (this.strictLocking == null) { return null; } + @Override + public void prepareSingletonBootstrap() { + this.mainThreadPrefix = getThreadNamePrefix(); + } + @Override public void preInstantiateSingletons() throws BeansException { if (logger.isTraceEnabled()) { @@ -1098,11 +1109,12 @@ public void preInstantiateSingletons() throws BeansException { List beanNames = new ArrayList<>(this.beanDefinitionNames); // Trigger initialization of all non-lazy singleton beans... - List> futures = new ArrayList<>(); - this.preInstantiationThread.set(PreInstantiation.MAIN); - this.mainThreadPrefix = getThreadNamePrefix(); + if (this.mainThreadPrefix == null) { + this.mainThreadPrefix = getThreadNamePrefix(); + } try { + List> futures = new ArrayList<>(); for (String beanName : beanNames) { RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); if (!mbd.isAbstract() && mbd.isSingleton()) { @@ -1112,21 +1124,20 @@ public void preInstantiateSingletons() throws BeansException { } } } + if (!futures.isEmpty()) { + try { + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } + catch (CompletionException ex) { + ReflectionUtils.rethrowRuntimeException(ex.getCause()); + } + } } finally { this.mainThreadPrefix = null; this.preInstantiationThread.remove(); } - if (!futures.isEmpty()) { - try { - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - } - catch (CompletionException ex) { - ReflectionUtils.rethrowRuntimeException(ex.getCause()); - } - } - // Trigger post-initialization callback for all applicable beans... for (String beanName : beanNames) { Object singletonInstance = getSingleton(beanName, false); @@ -1309,7 +1320,7 @@ else if (isConfigurationFrozen()) { // Cache a primary marker for the given bean. if (beanDefinition.isPrimary()) { - this.primaryBeanNames.add(beanName); + this.primaryBeanNamesWithType.put(beanName, Void.class); } } @@ -1401,7 +1412,7 @@ protected void resetBeanDefinition(String beanName) { destroySingleton(beanName); // 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. for (MergedBeanDefinitionPostProcessor processor : getBeanPostProcessorCache().mergedDefinition) { @@ -1451,11 +1462,30 @@ protected void checkForAliasCircle(String name, String alias) { } } + @Override + protected void addSingleton(String beanName, Object singletonObject) { + super.addSingleton(beanName, singletonObject); + + Predicate> 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 public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException { super.registerSingleton(beanName, singletonObject); + updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName)); - clearByTypeCache(); + this.allBeanNamesByType.remove(Object.class); + this.singletonBeanNamesByType.remove(Object.class); } @Override @@ -1621,7 +1651,7 @@ else if (descriptor.supportsLazyResolution()) { return doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); } - @SuppressWarnings("NullAway") // Dataflow analysis limitation + @SuppressWarnings("NullAway") // Dataflow analysis limitation public @Nullable Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { @@ -1947,7 +1977,8 @@ protected Map findAutowireCandidates( DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch(); for (String candidate : candidateNames) { if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor) && - (!multiple || getAutowireCandidateResolver().hasQualifier(descriptor))) { + (!multiple || matchesBeanName(candidate, descriptor.getDependencyName()) || + getAutowireCandidateResolver().hasQualifier(descriptor))) { addCandidateEntry(result, candidate, descriptor, requiredType); } } @@ -2062,8 +2093,9 @@ else if (containsSingleton(candidateName) || boolean candidateLocal = containsBeanDefinition(candidateBeanName); boolean primaryLocal = containsBeanDefinition(primaryBeanName); if (candidateLocal == primaryLocal) { - throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), - "more than one 'primary' bean found among candidates: " + candidates.keySet()); + String message = "more than one 'primary' bean found among candidates: " + candidates.keySet(); + logger.trace(message); + throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), message); } else if (candidateLocal) { primaryBeanName = candidateBeanName; @@ -2213,12 +2245,12 @@ private String determineDefaultCandidate(Map candidates) { } /** - * Determine whether the given candidate name matches the bean name or the aliases + * Determine whether the given dependency name matches the bean name or the aliases * stored in this bean definition. */ - protected boolean matchesBeanName(String beanName, @Nullable String candidateName) { - return (candidateName != null && - (candidateName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), candidateName))); + protected boolean matchesBeanName(String beanName, @Nullable String dependencyName) { + return (dependencyName != null && + (dependencyName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), dependencyName))); } /** @@ -2238,8 +2270,12 @@ private boolean isSelfReference(@Nullable String beanName, @Nullable String cand * not matching the given bean name. */ private boolean hasPrimaryConflict(String beanName, Class dependencyType) { - for (String candidate : this.primaryBeanNames) { - if (isTypeMatch(candidate, dependencyType) && !candidate.equals(beanName)) { + for (Map.Entry> candidate : this.primaryBeanNamesWithType.entrySet()) { + 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; } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index 420e3eb9a370..550d7f04ad49 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import org.apache.commons.logging.Log; @@ -75,7 +76,7 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { private static final Log logger = LogFactory.getLog(DisposableBeanAdapter.class); - private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent( "org.reactivestreams.Publisher", DisposableBeanAdapter.class.getClassLoader()); @@ -319,7 +320,7 @@ else if (returnValue instanceof Future future) { future.get(); logDestroyMethodCompletion(destroyMethod, true); } - else if (!reactiveStreamsPresent || !new ReactiveDestroyMethodHandler().await(destroyMethod, returnValue)) { + else if (!REACTIVE_STREAMS_PRESENT || !new ReactiveDestroyMethodHandler().await(destroyMethod, returnValue)) { if (logger.isDebugEnabled()) { logger.debug("Unknown return value type from custom destroy method '" + destroyMethod.getName() + "' on bean with name '" + this.beanName + "': " + returnValue.getClass()); @@ -412,14 +413,29 @@ public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefin String destroyMethodName = beanDefinition.resolvedDestroyMethodName; if (destroyMethodName == null) { destroyMethodName = beanDefinition.getDestroyMethodName(); - boolean autoCloseable = (AutoCloseable.class.isAssignableFrom(target)); + boolean autoCloseable = AutoCloseable.class.isAssignableFrom(target); + boolean executorService = ExecutorService.class.isAssignableFrom(target); if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) || - (destroyMethodName == null && autoCloseable)) { + (destroyMethodName == null && (autoCloseable || executorService))) { // Only perform destroy method inference in case of the bean // not explicitly implementing the DisposableBean interface destroyMethodName = null; if (!(DisposableBean.class.isAssignableFrom(target))) { - if (autoCloseable) { + if (executorService) { + destroyMethodName = SHUTDOWN_METHOD_NAME; + try { + // On JDK 19+, avoid the ExecutorService-level AutoCloseable default implementation + // which awaits task termination for 1 day, even for delayed tasks such as cron jobs. + // Custom close() implementations in ExecutorService subclasses are still accepted. + if (target.getMethod(CLOSE_METHOD_NAME).getDeclaringClass() != ExecutorService.class) { + destroyMethodName = CLOSE_METHOD_NAME; + } + } + catch (NoSuchMethodException ex) { + // Ignore - stick with shutdown() + } + } + else if (autoCloseable) { destroyMethodName = CLOSE_METHOD_NAME; } else { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java index b8431437fa33..3e428b976fa2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java @@ -130,45 +130,41 @@ protected Object getObjectFromFactoryBean(FactoryBean factory, @Nullable Clas locked = (lockFlag && this.singletonLock.tryLock()); } try { - // A SmartFactoryBean may return multiple object types -> do not cache. - boolean smart = (factory instanceof SmartFactoryBean); - Object object = (!smart ? this.factoryBeanObjectCache.get(beanName) : null); - if (object == null) { - object = doGetObjectFromFactoryBean(factory, requiredType, beanName); - // Only post-process and store if not put there already during getObject() call above - // (for example, because of circular reference processing triggered by custom getBean calls) - Object alreadyThere = (!smart ? this.factoryBeanObjectCache.get(beanName) : null); - if (alreadyThere != null) { - object = alreadyThere; + if (factory instanceof SmartFactoryBean) { + // A SmartFactoryBean may return multiple object types -> do not cache. + // Also, a SmartFactoryBean needs to be thread-safe -> no synchronization necessary. + Object object = doGetObjectFromFactoryBean(factory, requiredType, beanName); + if (shouldPostProcess) { + object = postProcessObjectFromSingletonFactoryBean(object, beanName, locked); } - else { - if (shouldPostProcess) { - if (locked) { - if (isSingletonCurrentlyInCreation(beanName)) { - // Temporarily return non-post-processed object, not storing it yet - return object; - } - beforeSingletonCreation(beanName); - } - try { - object = postProcessObjectFromFactoryBean(object, beanName); - } - catch (Throwable ex) { - throw new BeanCreationException(beanName, - "Post-processing of FactoryBean's singleton object failed", ex); + return object; + } + else { + // Defensively synchronize against non-thread-safe FactoryBean.getObject() implementations, + // potentially to be called from a background thread while the main thread currently calls + // the same getObject() method within the singleton lock. + synchronized (factory) { + Object object = this.factoryBeanObjectCache.get(beanName); + if (object == null) { + object = doGetObjectFromFactoryBean(factory, requiredType, beanName); + // Only post-process and store if not put there already during getObject() call above + // (for example, because of circular reference processing triggered by custom getBean calls) + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; } - finally { - if (locked) { - afterSingletonCreation(beanName); + else { + if (shouldPostProcess) { + object = postProcessObjectFromSingletonFactoryBean(object, beanName, locked); + } + if (containsSingleton(beanName)) { + this.factoryBeanObjectCache.put(beanName, object); } } } - if (!smart && containsSingleton(beanName)) { - this.factoryBeanObjectCache.put(beanName, object); - } + return object; } } - return object; } finally { if (locked) { @@ -225,6 +221,31 @@ private Object doGetObjectFromFactoryBean(FactoryBean factory, @Nullable Clas return object; } + /** + * Post-process the given object instance produced by a singleton FactoryBean. + */ + private Object postProcessObjectFromSingletonFactoryBean(Object object, String beanName, boolean locked) { + if (locked) { + if (isSingletonCurrentlyInCreation(beanName)) { + // Temporarily return non-post-processed object, not storing it yet + return object; + } + beforeSingletonCreation(beanName); + } + try { + return postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); + } + finally { + if (locked) { + afterSingletonCreation(beanName); + } + } + } + /** * Post-process the given object that has been obtained from the FactoryBean. * The resulting object will get exposed for bean references. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java index 26f19262fc92..3b2c5e9c1899 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java @@ -39,6 +39,7 @@ import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.SmartFactoryBean; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; @@ -199,6 +200,11 @@ public ObjectProvider getBeanProvider(ResolvableType requiredType) { return getBeanProvider(requiredType, true); } + @Override + public ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType) { + return getBeanProvider(ResolvableType.forType(requiredType), true); + } + @Override public boolean containsBean(String name) { return this.beans.containsKey(name); diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java index 3fc5065b5991..de26e7b70671 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PathEditor.java @@ -103,12 +103,12 @@ public void setAsText(String text) throws IllegalArgumentException { if (resource == null) { setValue(null); } - else if (nioPathCandidate && !resource.exists()) { + else if (nioPathCandidate && (!resource.isFile() || !resource.exists())) { setValue(Paths.get(text).normalize()); } else { try { - setValue(resource.getFile().toPath()); + setValue(resource.getFilePath()); } catch (IOException ex) { String msg = "Could not resolve \"" + text + "\" to 'java.nio.file.Path' for " + resource + ": " + diff --git a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt index a6a4a56a64fa..aeb68a5be71a 100644 --- a/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt @@ -168,12 +168,8 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean if (prototype) { it.prototype() } - val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); - if (resolvableType.hasGenerics()) { - it.targetType(resolvableType) - } } - registry.registerBean(name, T::class.java, customizer) + registry.registerBean(name, object: ParameterizedTypeReference() {}, customizer) } /** @@ -234,12 +230,8 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean if (prototype) { it.prototype() } - val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); - if (resolvableType.hasGenerics()) { - it.targetType(resolvableType) - } } - return registry.registerBean(T::class.java, customizer) + return registry.registerBean(object: ParameterizedTypeReference() {}, customizer) } /** @@ -304,12 +296,8 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean it.supplier { SupplierContextDsl(it, env).supplier() } - val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); - if (resolvableType.hasGenerics()) { - it.targetType(resolvableType) - } } - registry.registerBean(name, T::class.java, customizer) + registry.registerBean(name, object: ParameterizedTypeReference() {}, customizer) } inline fun registerBean(autowirable: Boolean = true, @@ -372,702 +360,10 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean it.supplier { SupplierContextDsl(it, env).supplier() } - val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); - if (resolvableType.hasGenerics()) { - it.targetType(resolvableType) - } } - return registry.registerBean(T::class.java, customizer) + return registry.registerBean(object: ParameterizedTypeReference() {}, customizer) } - // Function with 0 parameter - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f]. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: () -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke() - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f]. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: () -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke() - } - - // Function with 1 parameter - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean()) - } - - // Function with 2 parameters - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean()) - } - - // Function with 3 parameters - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean()) - } - - // Function with 4 parameters - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean()) - } - - // Function with 5 parameters - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean()) - } - - // Function with 6 parameters - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E, F) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean(), bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E, F) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean(), bean()) - } - - // Function with 7 parameters - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E, F, G) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E, F, G) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean()) - } - - // Function with 8 parameters - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E, F, G, H) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E, F, G, H) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean()) - } - - // Function with 9 parameters - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E, F, G, H, I) -> T, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean()) - } - - /** - * Register a bean of type [T] which will be instantiated by invoking the - * provided [function][f] with its parameters autowired by type. - * @param T the bean type - * @param name the name of the bean - * @param autowirable set whether this bean is a candidate for getting - * autowired into some other bean - * @param backgroundInit set whether this bean allows for instantiation - * on a background thread - * @param description a human-readable description of this bean - * @param fallback set whether this bean is a fallback autowire candidate - * @param infrastructure set whether this bean has an infrastructure role, - * meaning it has no relevance to the end-user - * @param lazyInit set whether this bean is lazily initialized - * @param order the sort order of this bean - * @param primary set whether this bean is a primary autowire candidate - * @param prototype set whether this bean has a prototype scope - */ - inline fun registerBean( - crossinline f: (A, B, C, D, E, F, G, H, I) -> T, - name: String, - autowirable: Boolean = true, - backgroundInit: Boolean = false, - description: String? = null, - fallback: Boolean = false, - infrastructure: Boolean = false, - lazyInit: Boolean = false, - order: Int? = null, - primary: Boolean = false, - prototype: Boolean = false) = - registerBean(name, autowirable, backgroundInit, description, fallback, infrastructure, lazyInit, order, primary, prototype) { - f.invoke(bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean(), bean()) - } - /** * Context available from the bean instance supplier designed to give access @@ -1094,7 +390,7 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean * @return a corresponding provider handle */ inline fun beanProvider() : ObjectProvider = - context.beanProvider(ResolvableType.forType((object : ParameterizedTypeReference() {}).type)) + context.beanProvider(object : ParameterizedTypeReference() {}) } override fun register(registry: BeanRegistry, env: Environment) { diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd index 27b7b3434714..3c3e00c14f24 100644 --- a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.dtd @@ -330,7 +330,7 @@ list or are supposed to be matched generically by type. Note: A single generic argument value will just be used once, rather than - potentially matched multiple times (as of Spring 1.1). + potentially matched multiple times. constructor-arg elements are also used in conjunction with the factory-method element to construct beans using static or instance factory methods. diff --git a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.xsd b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.xsd index a27c3e9f80e2..a04ccbd5a670 100644 --- a/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.xsd +++ b/spring-beans/src/main/resources/org/springframework/beans/factory/xml/spring-beans.xsd @@ -567,7 +567,7 @@ argument list or are supposed to be matched generically by type. Note: A single generic argument value will just be used once, rather - than potentially matched multiple times (as of Spring 1.1). + than potentially matched multiple times. constructor-arg elements are also used in conjunction with the factory-method element to construct beans using static or instance diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java index e0418b83f2e3..a221cdc8f852 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -472,7 +472,7 @@ void setEmptyPropertyValues() { assertThat(target.getAge()).as("age is OK").isEqualTo(age); assertThat(name).as("name is OK").isEqualTo(target.getName()); accessor.setPropertyValues(new MutablePropertyValues()); - // Check its unchanged + // Check it's unchanged assertThat(target.getAge()).as("age is OK").isEqualTo(age); assertThat(name).as("name is OK").isEqualTo(target.getName()); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java index aedbc4874190..4ab5ced5b1bc 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/BeanFactoryUtilsTests.java @@ -195,7 +195,7 @@ void testFindsBeansOfTypeWithDefaultFactory() { assertThat(beans.get("t2")).isEqualTo(t2); assertThat(beans.get("t3")).isEqualTo(t3.getObject()); assertThat(beans.get("t4")).isInstanceOf(TestBean.class); - // t3 and t4 are found here as of Spring 2.0, since they are pre-registered + // t3 and t4 are found here, since they are pre-registered // singleton instances, while testFactory1 and testFactory are *not* found // because they are FactoryBean definitions that haven't been initialized yet. diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index eb5e0da5746b..a582e69a286d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -87,6 +87,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; import static org.assertj.core.api.Assertions.assertThat; @@ -1807,7 +1808,7 @@ void getBeanByTypeWithMultiplePrimary() { assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) .isThrownBy(() -> lbf.getBean(TestBean.class)) - .withMessageContaining("more than one 'primary'"); + .withMessageEndingWith("more than one 'primary' bean found among candidates: [bd1, bd2]"); } @Test @@ -2131,7 +2132,7 @@ void getBeanByTypeInstanceWithMultiplePrimary() { assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) .isThrownBy(() -> lbf.getBean(ConstructorDependency.class, 42)) - .withMessageContaining("more than one 'primary'"); + .withMessageEndingWith("more than one 'primary' bean found among candidates: [bd1, bd2]"); } @Test @@ -3211,6 +3212,33 @@ void nonPublicEnum() { 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); + + lbf.registerSingleton("bd3", new Object()); + assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames); + assertThat(lbf.getBeanNamesForType(Object.class)).containsExactly(StringUtils.addStringToArray(allBeanNames, "bd3")); + } + private int registerBeanDefinitions(Properties p) { return registerBeanDefinitions(p, null); @@ -3427,7 +3455,7 @@ public void setName(String name) { } public TestBean create() { - TestBean tb = new TestBean(); + DerivedTestBean tb = new DerivedTestBean(); tb.setName(this.name); return tb; } @@ -3655,11 +3683,11 @@ private static class FactoryBeanDependentBean { private FactoryBean factoryBean; - public final FactoryBean getFactoryBean() { + public FactoryBean getFactoryBean() { return this.factoryBean; } - public final void setFactoryBean(final FactoryBean factoryBean) { + public void setFactoryBean(FactoryBean factoryBean) { this.factoryBean = factoryBean; } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java index 765e22681378..c97e6ce4e098 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessorTests.java @@ -1279,88 +1279,6 @@ void constructorInjectionWithMap() { assertThat(bean.getTestBean().get("testBean2")).isNull(); } - @Test - void fieldInjectionWithMap() { - RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class); - bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); - bf.registerBeanDefinition("annotatedBean", bd); - TestBean tb1 = new TestBean("tb1"); - TestBean tb2 = new TestBean("tb2"); - bf.registerSingleton("testBean1", tb1); - bf.registerSingleton("testBean2", tb2); - bf.registerAlias("testBean1", "testBean"); - - MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); - assertThat(bean.getTestBeanMap()).hasSize(2); - assertThat(bean.getTestBeanMap()).containsKey("testBean1"); - assertThat(bean.getTestBeanMap()).containsKey("testBean2"); - assertThat(bean.getTestBeanMap()).containsValue(tb1); - assertThat(bean.getTestBeanMap()).containsValue(tb2); - - bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); - assertThat(bean.getTestBeanMap()).hasSize(2); - assertThat(bean.getTestBeanMap()).containsKey("testBean1"); - assertThat(bean.getTestBeanMap()).containsKey("testBean2"); - assertThat(bean.getTestBeanMap()).containsValue(tb1); - assertThat(bean.getTestBeanMap()).containsValue(tb2); - } - - @Test - void methodInjectionWithMap() { - RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class); - bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); - bf.registerBeanDefinition("annotatedBean", bd); - TestBean tb = new TestBean(); - bf.registerSingleton("testBean", tb); - - MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); - assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap()).containsKey("testBean"); - assertThat(bean.getTestBeanMap()).containsValue(tb); - assertThat(bean.getTestBean()).isSameAs(tb); - - bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); - assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap()).containsKey("testBean"); - assertThat(bean.getTestBeanMap()).containsValue(tb); - assertThat(bean.getTestBean()).isSameAs(tb); - } - - @Test - void methodInjectionWithMapAndMultipleMatches() { - bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); - bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); - bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type") - .isThrownBy(() -> bf.getBean("annotatedBean")) - .satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class)); - } - - @Test - void methodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() { - bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); - bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); - RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class); - rbd2.setAutowireCandidate(false); - bf.registerBeanDefinition("testBean2", rbd2); - - MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); - TestBean tb = bf.getBean("testBean1", TestBean.class); - assertThat(bean.getTestBeanMap()).hasSize(1); - assertThat(bean.getTestBeanMap()).containsKey("testBean1"); - assertThat(bean.getTestBeanMap()).containsValue(tb); - assertThat(bean.getTestBean()).isSameAs(tb); - } - - @Test - void methodInjectionWithMapAndNoMatches() { - bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); - - MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); - assertThat(bean.getTestBeanMap()).isNull(); - assertThat(bean.getTestBean()).isNull(); - } - @Test void constructorInjectionWithTypedMapAsBean() { RootBeanDefinition bd = new RootBeanDefinition(MapConstructorInjectionBean.class); @@ -1413,6 +1331,19 @@ void constructorInjectionWithCustomMapAsBean() { @Test void constructorInjectionWithPlainHashMapAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(NamedMapConstructorInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + bf.registerBeanDefinition("testBeanMap", new RootBeanDefinition(HashMap.class)); + + NamedMapConstructorInjectionBean bean = bf.getBean("annotatedBean", NamedMapConstructorInjectionBean.class); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap")); + bean = bf.getBean("annotatedBean", NamedMapConstructorInjectionBean.class); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap")); + } + + @Test + void constructorInjectionWithQualifiedPlainHashMapAsBean() { RootBeanDefinition bd = new RootBeanDefinition(QualifiedMapConstructorInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("annotatedBean", bd); @@ -1501,6 +1432,114 @@ void constructorInjectionWithSortedSetFallback() { assertThat(bean.getTestBeanSet()).contains(tb1, tb2); } + @Test + void fieldInjectionWithMap() { + RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb1 = new TestBean("tb1"); + TestBean tb2 = new TestBean("tb2"); + bf.registerSingleton("testBean1", tb1); + bf.registerSingleton("testBean2", tb2); + bf.registerAlias("testBean1", "testBean"); + + MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); + assertThat(bean.getTestBeanMap()).hasSize(2); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsKey("testBean2"); + assertThat(bean.getTestBeanMap()).containsValue(tb1); + assertThat(bean.getTestBeanMap()).containsValue(tb2); + + bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class); + assertThat(bean.getTestBeanMap()).hasSize(2); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsKey("testBean2"); + assertThat(bean.getTestBeanMap()).containsValue(tb1); + assertThat(bean.getTestBeanMap()).containsValue(tb2); + } + + @Test + void methodInjectionWithMap() { + RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + TestBean tb = new TestBean(); + bf.registerSingleton("testBean", tb); + + MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); + assertThat(bean.getTestBeanMap()).hasSize(1); + assertThat(bean.getTestBeanMap()).containsKey("testBean"); + assertThat(bean.getTestBeanMap()).containsValue(tb); + assertThat(bean.getTestBean()).isSameAs(tb); + + bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); + assertThat(bean.getTestBeanMap()).hasSize(1); + assertThat(bean.getTestBeanMap()).containsKey("testBean"); + assertThat(bean.getTestBeanMap()).containsValue(tb); + assertThat(bean.getTestBean()).isSameAs(tb); + } + + @Test + void methodInjectionWithMapAndMultipleMatches() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); + bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type") + .isThrownBy(() -> bf.getBean("annotatedBean")) + .satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class)); + } + + @Test + void methodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); + bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class)); + RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class); + rbd2.setAutowireCandidate(false); + bf.registerBeanDefinition("testBean2", rbd2); + + MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); + TestBean tb = bf.getBean("testBean1", TestBean.class); + assertThat(bean.getTestBeanMap()).hasSize(1); + assertThat(bean.getTestBeanMap()).containsKey("testBean1"); + assertThat(bean.getTestBeanMap()).containsValue(tb); + assertThat(bean.getTestBean()).isSameAs(tb); + } + + @Test + void methodInjectionWithMapAndNoMatches() { + bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class)); + + MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class); + assertThat(bean.getTestBeanMap()).isNull(); + assertThat(bean.getTestBean()).isNull(); + } + + @Test + void methodInjectionWithPlainHashMapAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(NamedMapMethodInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + bf.registerBeanDefinition("testBeanMap", new RootBeanDefinition(HashMap.class)); + + NamedMapMethodInjectionBean bean = bf.getBean("annotatedBean", NamedMapMethodInjectionBean.class); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap")); + bean = bf.getBean("annotatedBean", NamedMapMethodInjectionBean.class); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap")); + } + + @Test + void methodInjectionWithQualifiedPlainHashMapAsBean() { + RootBeanDefinition bd = new RootBeanDefinition(QualifiedMapMethodInjectionBean.class); + bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); + bf.registerBeanDefinition("annotatedBean", bd); + bf.registerBeanDefinition("myTestBeanMap", new RootBeanDefinition(HashMap.class)); + + QualifiedMapMethodInjectionBean bean = bf.getBean("annotatedBean", QualifiedMapMethodInjectionBean.class); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + bean = bf.getBean("annotatedBean", QualifiedMapMethodInjectionBean.class); + assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap")); + } + @Test void selfReference() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class)); @@ -3262,6 +3301,21 @@ public SortedMap getTestBeanMap() { } + public static class NamedMapConstructorInjectionBean { + + private Map testBeanMap; + + @Autowired + public NamedMapConstructorInjectionBean(Map testBeanMap) { + this.testBeanMap = testBeanMap; + } + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + public static class QualifiedMapConstructorInjectionBean { private Map testBeanMap; @@ -3361,6 +3415,37 @@ public Map getTestBeanMap() { } + public static class NamedMapMethodInjectionBean { + + private Map testBeanMap; + + @Autowired + public void setTestBeanMap(Map testBeanMap) { + this.testBeanMap = testBeanMap; + } + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + + public static class QualifiedMapMethodInjectionBean { + + private Map testBeanMap; + + @Autowired + @Qualifier("myTestBeanMap") + public void setTestBeanMap(Map testBeanMap) { + this.testBeanMap = testBeanMap; + } + + public Map getTestBeanMap() { + return this.testBeanMap; + } + } + + @SuppressWarnings("serial") public static class ObjectFactoryFieldInjectionBean implements Serializable { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java index 8756eadb0149..5e374429ad56 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorTests.java @@ -16,6 +16,7 @@ package org.springframework.beans.factory.aot; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,6 +35,7 @@ import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.AutowiredPropertyMarker; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -626,6 +628,22 @@ void generateBeanDefinitionMethodWhenCustomPropertyValueUsesCustomDelegate() { -> assertThat(customPropertyValue.value()).isEqualTo("test"))); } + @Test + void generateBeanDefinitionMethodWhenHasAutowiredPropertyGeneratesMethod() { + RootBeanDefinition beanDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(CustomBean.class).addAutowiredProperty("innerBean") + .getBeanDefinition(); + RegisteredBean registeredBean = registerBean(beanDefinition); + BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator( + this.methodGeneratorFactory, registeredBean, null, + Collections.emptyList()); + MethodReference method = generator.generateBeanDefinitionMethod( + this.generationContext, this.beanRegistrationsCode); + compile(method, (actual, compiled) -> + assertThat(actual.getPropertyValues().get("innerBean")) + .isSameAs(AutowiredPropertyMarker.INSTANCE)); + } + @Test void generateBeanDefinitionMethodWhenHasAotContributionsAppliesContributions() { RegisteredBean registeredBean = registerBean( diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java index e1f49f68c47d..0e24774cf03c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java @@ -40,6 +40,7 @@ import org.springframework.aot.generate.ValueCodeGenerator; import org.springframework.aot.generate.ValueCodeGeneratorDelegates; import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.AutowiredPropertyMarker; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.config.RuntimeBeanNameReference; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -486,4 +487,16 @@ void generatedWhenBeanReferenceByType() { } + @Nested + class AutowiredPropertyMarkerTests { + + @Test + void generateWhenAutowiredPropertyMarker() { + compile(AutowiredPropertyMarker.INSTANCE, (instance, compiler) -> + assertThat(instance).isInstanceOf(AutowiredPropertyMarker.class) + .isSameAs(AutowiredPropertyMarker.INSTANCE)); + } + + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java index ed8c7ed96d06..b440cff9f13c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorTests.java @@ -408,6 +408,16 @@ void generateWhenTargetFactoryMethodReturnTypeIsDeprecated() { compileAndCheckWarnings(beanDefinition); } + @Test + void generateWhenTargetFactoryMethodIsProtectedAndReturnTypeIsDeprecated() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(DeprecatedBean.class) + .setFactoryMethodOnBean("deprecatedReturnTypeProtected", "config").getBeanDefinition(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(DeprecatedMemberConfiguration.class).getBeanDefinition()); + compileAndCheckWarnings(beanDefinition); + } + private void compileAndCheckWarnings(BeanDefinition beanDefinition) { assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition, ((instanceSupplier, compiled) -> {}))); @@ -454,6 +464,26 @@ void generateWhenTargetFactoryMethodParameterIsDeprecatedForRemoval() { compileAndCheckWarnings(beanDefinition); } + @Test + void generateWhenTargetFactoryMethodReturnTypeIsDeprecatedForRemoval() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(DeprecatedForRemovalBean.class) + .setFactoryMethodOnBean("deprecatedReturnType", "config").getBeanDefinition(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition()); + compileAndCheckWarnings(beanDefinition); + } + + @Test + void generateWhenTargetFactoryMethodIsProtectedAndReturnTypeIsDeprecatedForRemoval() { + BeanDefinition beanDefinition = BeanDefinitionBuilder + .rootBeanDefinition(DeprecatedForRemovalBean.class) + .setFactoryMethodOnBean("deprecatedReturnTypeProtected", "config").getBeanDefinition(); + beanFactory.registerBeanDefinition("config", BeanDefinitionBuilder + .genericBeanDefinition(DeprecatedForRemovalMemberConfiguration.class).getBeanDefinition()); + compileAndCheckWarnings(beanDefinition); + } + private void compileAndCheckWarnings(BeanDefinition beanDefinition) { assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, beanDefinition, ((instanceSupplier, compiled) -> {}))); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java index 716afff48452..c8b75134a3cf 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java @@ -45,6 +45,7 @@ import org.springframework.beans.testfixture.beans.GenericSetOfIntegerBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.OverridingClassLoader; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.Order; @@ -792,9 +793,9 @@ void genericMatchingWithFullTypeDifferentiation(Class factoryClass) { assertThat(doubleStoreNames).containsExactly("store1"); assertThat(floatStoreNames).containsExactly("store2"); - ObjectProvider> numberStoreProvider = bf.getBeanProvider(ResolvableType.forClass(NumberStore.class)); - ObjectProvider> doubleStoreProvider = bf.getBeanProvider(ResolvableType.forClassWithGenerics(NumberStore.class, Double.class)); - ObjectProvider> floatStoreProvider = bf.getBeanProvider(ResolvableType.forClassWithGenerics(NumberStore.class, Float.class)); + ObjectProvider> numberStoreProvider = bf.getBeanProvider(new ParameterizedTypeReference<>() {}); + ObjectProvider> doubleStoreProvider = bf.getBeanProvider(new ParameterizedTypeReference<>() {}); + ObjectProvider> floatStoreProvider = bf.getBeanProvider(new ParameterizedTypeReference<>() {}); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(numberStoreProvider::getObject); assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(numberStoreProvider::getIfAvailable); assertThat(numberStoreProvider.getIfUnique()).isNull(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java index 51461235a883..c1ceba107961 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java @@ -24,7 +24,6 @@ import org.springframework.beans.factory.BeanRegistry; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ResolvableType; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; @@ -206,18 +205,10 @@ void customSupplier() { } @Test - void customTargetTypeFromResolvableType() { - BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, env, TargetTypeBeanRegistrar.class); - new TargetTypeBeanRegistrar().register(adapter, env); - RootBeanDefinition beanDefinition = (RootBeanDefinition)this.beanFactory.getBeanDefinition("fooSupplierFromResolvableType"); - assertThat(beanDefinition.getResolvableType().resolveGeneric(0)).isEqualTo(Foo.class); - } - - @Test - void customTargetTypeFromTypeReference() { - BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, env, TargetTypeBeanRegistrar.class); - new TargetTypeBeanRegistrar().register(adapter, env); - RootBeanDefinition beanDefinition = (RootBeanDefinition)this.beanFactory.getBeanDefinition("fooSupplierFromTypeReference"); + void genericType() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, env, GenericTypeBeanRegistrar.class); + new GenericTypeBeanRegistrar().register(adapter, env); + RootBeanDefinition beanDefinition = (RootBeanDefinition)this.beanFactory.getBeanDefinition("fooSupplier"); assertThat(beanDefinition.getResolvableType().resolveGeneric(0)).isEqualTo(Foo.class); } @@ -325,15 +316,11 @@ public void register(BeanRegistry registry, Environment env) { } } - private static class TargetTypeBeanRegistrar implements BeanRegistrar { + private static class GenericTypeBeanRegistrar implements BeanRegistrar { @Override public void register(BeanRegistry registry, Environment env) { - registry.registerBean("fooSupplierFromResolvableType", Foo.class, - spec -> spec.targetType(ResolvableType.forClassWithGenerics(Supplier.class, Foo.class))); - ParameterizedTypeReference> type = new ParameterizedTypeReference<>() {}; - registry.registerBean("fooSupplierFromTypeReference", Supplier.class, - spec -> spec.targetType(type)); + registry.registerBean("fooSupplier", new ParameterizedTypeReference>() {}); } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java index 056597442ba4..3d71b6d2420e 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/RootBeanDefinitionTests.java @@ -17,6 +17,7 @@ package org.springframework.beans.factory.support; import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; import org.junit.jupiter.api.Test; @@ -59,13 +60,38 @@ void setInstanceDoesNotOverrideResolvedFactoryMethodWithNull() { } @Test - void resolveDestroyMethodWithMatchingCandidateReplacedInferredVaue() { + void resolveDestroyMethodWithMatchingCandidateReplacedForCloseMethod() { RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithCloseMethod.class); beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); beanDefinition.resolveDestroyMethodIfNecessary(); assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close"); } + @Test + void resolveDestroyMethodWithMatchingCandidateReplacedForShutdownMethod() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithShutdownMethod.class); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown"); + } + + @Test + void resolveDestroyMethodWithMatchingCandidateReplacedForExecutorService() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorService.class); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("shutdown"); + // even on JDK 19+ where the ExecutorService interface declares a default AutoCloseable implementation + } + + @Test + void resolveDestroyMethodWithMatchingCandidateReplacedForAutoCloseableExecutorService() { + RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanImplementingExecutorServiceAndAutoCloseable.class); + beanDefinition.setDestroyMethodName(AbstractBeanDefinition.INFER_METHOD); + beanDefinition.resolveDestroyMethodIfNecessary(); + assertThat(beanDefinition.getDestroyMethodNames()).containsExactly("close"); + } + @Test void resolveDestroyMethodWithNoCandidateSetDestroyMethodNameToNull() { RootBeanDefinition beanDefinition = new RootBeanDefinition(BeanWithNoDestroyMethod.class); @@ -90,6 +116,25 @@ public void close() { } + static class BeanWithShutdownMethod { + + public void shutdown() { + } + } + + + abstract static class BeanImplementingExecutorService implements ExecutorService { + } + + + abstract static class BeanImplementingExecutorServiceAndAutoCloseable implements ExecutorService, AutoCloseable { + + @Override + public void close() { + } + } + + static class BeanWithNoDestroyMethod { } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java index 5d47ea830841..537f6cd42b2c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/UtilNamespaceHandlerTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.config.FieldRetrievingFactoryBean; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.beans.factory.parsing.ComponentDefinition; @@ -46,7 +47,6 @@ * @author Juergen Hoeller * @author Mark Fisher */ -@SuppressWarnings("rawtypes") class UtilNamespaceHandlerTests { private DefaultListableBeanFactory beanFactory; @@ -55,7 +55,7 @@ class UtilNamespaceHandlerTests { @BeforeEach - void setUp() { + void setup() { this.beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); reader.setEventListener(this.listener); @@ -109,17 +109,17 @@ void testNestedPropertyPath() { @Test void testSimpleMap() { - Map map = (Map) this.beanFactory.getBean("simpleMap"); + Map map = (Map) this.beanFactory.getBean("simpleMap"); assertThat(map.get("foo")).isEqualTo("bar"); - Map map2 = (Map) this.beanFactory.getBean("simpleMap"); + Map map2 = (Map) this.beanFactory.getBean("simpleMap"); assertThat(map).isSameAs(map2); } @Test void testScopedMap() { - Map map = (Map) this.beanFactory.getBean("scopedMap"); + Map map = (Map) this.beanFactory.getBean("scopedMap"); assertThat(map.get("foo")).isEqualTo("bar"); - Map map2 = (Map) this.beanFactory.getBean("scopedMap"); + Map map2 = (Map) this.beanFactory.getBean("scopedMap"); assertThat(map2.get("foo")).isEqualTo("bar"); assertThat(map).isNotSameAs(map2); } @@ -164,17 +164,23 @@ void testScopedSet() { } @Test - void testMapWithRef() { - Map map = (Map) this.beanFactory.getBean("mapWithRef"); + void testMapWithRef() throws Exception { + Map map = (Map) this.beanFactory.getBean("mapWithRef"); assertThat(map).isInstanceOf(TreeMap.class); assertThat(map.get("bean")).isEqualTo(this.beanFactory.getBean("testBean")); + assertThat(this.beanFactory.resolveDependency( + new DependencyDescriptor(getClass().getDeclaredField("mapWithRef"), true), null)) + .isSameAs(map); } @Test - void testMapWithTypes() { - Map map = (Map) this.beanFactory.getBean("mapWithTypes"); + void testMapWithTypes() throws Exception { + Map map = (Map) this.beanFactory.getBean("mapWithTypes"); assertThat(map).isInstanceOf(LinkedCaseInsensitiveMap.class); assertThat(map.get("bean")).isEqualTo(this.beanFactory.getBean("testBean")); + assertThat(this.beanFactory.resolveDependency( + new DependencyDescriptor(getClass().getDeclaredField("mapWithTypes"), true), null)) + .isSameAs(map); } @Test @@ -240,11 +246,11 @@ void testNestedInCollections() { void testCircularCollections() { TestBean bean = (TestBean) this.beanFactory.getBean("circularCollectionsBean"); - assertThat(bean.getSomeList()).singleElement().isEqualTo(bean); - assertThat(bean.getSomeSet()).singleElement().isEqualTo(bean); + assertThat(bean.getSomeList()).singleElement().isSameAs(bean); + assertThat(bean.getSomeSet()).singleElement().isSameAs(bean); assertThat(bean.getSomeMap()).hasSize(1).allSatisfy((key, value) -> { assertThat(key).isEqualTo("foo"); - assertThat(value).isEqualTo(bean); + assertThat(value).isSameAs(bean); }); } @@ -255,17 +261,17 @@ void testCircularCollectionBeansStartingWithList() { List list = bean.getSomeList(); assertThat(Proxy.isProxyClass(list.getClass())).isTrue(); - assertThat(list).singleElement().isEqualTo(bean); + assertThat(list).singleElement().isSameAs(bean); Set set = bean.getSomeSet(); assertThat(Proxy.isProxyClass(set.getClass())).isFalse(); - assertThat(set).singleElement().isEqualTo(bean); + assertThat(set).singleElement().isSameAs(bean); Map map = bean.getSomeMap(); assertThat(Proxy.isProxyClass(map.getClass())).isFalse(); assertThat(map).hasSize(1).allSatisfy((key, value) -> { assertThat(key).isEqualTo("foo"); - assertThat(value).isEqualTo(bean); + assertThat(value).isSameAs(bean); }); } @@ -276,17 +282,17 @@ void testCircularCollectionBeansStartingWithSet() { List list = bean.getSomeList(); assertThat(Proxy.isProxyClass(list.getClass())).isFalse(); - assertThat(list).singleElement().isEqualTo(bean); + assertThat(list).singleElement().isSameAs(bean); Set set = bean.getSomeSet(); assertThat(Proxy.isProxyClass(set.getClass())).isTrue(); - assertThat(set).singleElement().isEqualTo(bean); + assertThat(set).singleElement().isSameAs(bean); Map map = bean.getSomeMap(); assertThat(Proxy.isProxyClass(map.getClass())).isFalse(); assertThat(map).hasSize(1).allSatisfy((key, value) -> { assertThat(key).isEqualTo("foo"); - assertThat(value).isEqualTo(bean); + assertThat(value).isSameAs(bean); }); } @@ -297,17 +303,17 @@ void testCircularCollectionBeansStartingWithMap() { List list = bean.getSomeList(); assertThat(Proxy.isProxyClass(list.getClass())).isFalse(); - assertThat(list).singleElement().isEqualTo(bean); + assertThat(list).singleElement().isSameAs(bean); Set set = bean.getSomeSet(); assertThat(Proxy.isProxyClass(set.getClass())).isFalse(); - assertThat(set).singleElement().isEqualTo(bean); + assertThat(set).singleElement().isSameAs(bean); Map map = bean.getSomeMap(); assertThat(Proxy.isProxyClass(map.getClass())).isTrue(); assertThat(map).hasSize(1).allSatisfy((key, value) -> { assertThat(key).isEqualTo("foo"); - assertThat(value).isEqualTo(bean); + assertThat(value).isSameAs(bean); }); } @@ -372,4 +378,9 @@ void testLocalOverrideTrue() { assertThat(props).as("Incorrect property value").containsEntry("foo2", "local2"); } + + // For DependencyDescriptor resolution + Map mapWithRef; + Map mapWithTypes; + } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java index e4513a0fae4b..76ea5a53fed5 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java @@ -60,6 +60,8 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt private boolean jedi; + private String favoriteCafé; + private ITestBean spouse; private String touchy; @@ -210,6 +212,14 @@ public void setJedi(boolean jedi) { this.jedi = jedi; } + public String getFavoriteCafé() { + return this.favoriteCafé; + } + + public void setFavoriteCafé(String favoriteCafé) { + this.favoriteCafé = favoriteCafé; + } + @Override public ITestBean getSpouse() { return this.spouse; diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java index eabb36092149..68c077a8e816 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedForRemovalMemberConfiguration.java @@ -33,4 +33,14 @@ public String deprecatedParameter(DeprecatedForRemovalBean bean) { return bean.toString(); } + @SuppressWarnings("removal") + public DeprecatedForRemovalBean deprecatedReturnType() { + return new DeprecatedForRemovalBean(); + } + + @SuppressWarnings("removal") + DeprecatedForRemovalBean deprecatedReturnTypeProtected() { + return new DeprecatedForRemovalBean(); + } + } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java index 96c3a3453958..3508df0a88de 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/generator/deprecation/DeprecatedMemberConfiguration.java @@ -38,4 +38,9 @@ public DeprecatedBean deprecatedReturnType() { return new DeprecatedBean(); } + @SuppressWarnings("deprecation") + DeprecatedBean deprecatedReturnTypeProtected() { + return new DeprecatedBean(); + } + } diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java index 9092e9d21509..43b16ac84862 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/CandidateComponentsIndexer.java @@ -37,12 +37,13 @@ /** * Annotation {@link Processor} that writes a {@link CandidateComponentsMetadata} - * file for spring components. + * file for Spring components. * * @author Stephane Nicoll * @author Juergen Hoeller * @since 5.0 - * @deprecated as of 6.1, in favor of the AOT engine. + * @deprecated as of 6.1, in favor of the AOT engine and the forthcoming + * support for an AOT-generated Spring components index */ @Deprecated(since = "6.1", forRemoval = true) public class CandidateComponentsIndexer implements Processor { diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java index e8d66623e5d7..6bd49f7c3606 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/StandardStereotypesProvider.java @@ -25,8 +25,7 @@ /** * A {@link StereotypesProvider} that extracts a stereotype for each - * {@code jakarta.*} or {@code javax.*} annotation present on a class or - * interface. + * {@code jakarta.*} annotation present on a class or interface. * * @author Stephane Nicoll * @since 5.0 @@ -50,7 +49,7 @@ public Set getStereotypes(Element element) { } for (AnnotationMirror annotation : this.typeHelper.getAllAnnotationMirrors(element)) { String type = this.typeHelper.getType(annotation); - if (type.startsWith("jakarta.") || type.startsWith("javax.")) { + if (type.startsWith("jakarta.")) { stereotypes.add(type); } } diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java index f8190a73ac34..33f77e58ed5c 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java @@ -36,7 +36,7 @@ * operations through Caffeine's {@link AsyncCache}, when provided via the * {@link #CaffeineCache(String, AsyncCache, boolean)} constructor. * - *

    Requires Caffeine 3.0 or higher, as of Spring Framework 6.1. + *

    Requires Caffeine 3.0 or higher. * * @author Ben Manes * @author Juergen Hoeller diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java index 2625f1a455b8..15cf8e9e7721 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -53,7 +53,7 @@ * {@link AsyncCache}, when configured via {@link #setAsyncCacheMode}, * with early-determined cache misses. * - *

    Requires Caffeine 3.0 or higher, as of Spring Framework 6.1. + *

    Requires Caffeine 3.0 or higher. * * @author Ben Manes * @author Juergen Hoeller diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java index 6074f23e7fe2..020649a2b239 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java @@ -34,8 +34,6 @@ * {@link org.springframework.cache.Cache} implementation on top of a * {@link Cache javax.cache.Cache} instance. * - *

    Note: This class has been updated for JCache 1.0, as of Spring 4.0. - * * @author Juergen Hoeller * @author Stephane Nicoll * @since 3.2 diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java index f9f79c2bf80c..f5d0a9ee2ec9 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java @@ -32,8 +32,6 @@ * {@link org.springframework.cache.CacheManager} implementation * backed by a JCache {@link CacheManager javax.cache.CacheManager}. * - *

    Note: This class has been updated for JCache 1.0, as of Spring 4.0. - * * @author Juergen Hoeller * @author Stephane Nicoll * @since 3.2 diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java index 7da84a54cb77..ffa2767e6f87 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java @@ -34,8 +34,6 @@ * obtaining a pre-defined {@code CacheManager} by name through the standard * JCache {@link Caching javax.cache.Caching} class. * - *

    Note: This class has been updated for JCache 1.0, as of Spring 4.0. - * * @author Juergen Hoeller * @since 3.2 * @see javax.cache.Caching#getCachingProvider() diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java index 731e0b955139..1d171848a3e2 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java @@ -41,11 +41,12 @@ @Configuration(proxyBeanMethods = false) public abstract class AbstractJCacheConfiguration extends AbstractCachingConfiguration { - protected @Nullable Supplier<@Nullable CacheResolver> exceptionCacheResolver; + @SuppressWarnings("NullAway.Init") + protected Supplier<@Nullable CacheResolver> exceptionCacheResolver; @Override - @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128 + @SuppressWarnings("NullAway") // See https://github.com/uber/NullAway/issues/1290 protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerSupplier) { super.useCachingConfigurer(cachingConfigurerSupplier); this.exceptionCacheResolver = cachingConfigurerSupplier.adapt(config -> { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java index 572239aee41f..851302569260 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java @@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.Aware; import org.springframework.core.MethodClassKey; import org.springframework.util.ReflectionUtils; @@ -94,6 +95,10 @@ else if (cacheNull) { if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } + // Skip methods declared on BeanFactoryAware and co. + if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) { + return null; + } // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java index aba3f65c5db6..20b72690f3f5 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java @@ -50,11 +50,11 @@ public class DefaultJCacheOperationSource extends AnnotationJCacheOperationSource implements BeanFactoryAware, SmartInitializingSingleton { - private @Nullable SingletonSupplier cacheManager; + private @Nullable SingletonSupplier<@Nullable CacheManager> cacheManager; - private @Nullable SingletonSupplier cacheResolver; + private @Nullable SingletonSupplier<@Nullable CacheResolver> cacheResolver; - private @Nullable SingletonSupplier exceptionCacheResolver; + private @Nullable SingletonSupplier<@Nullable CacheResolver> exceptionCacheResolver; private SingletonSupplier keyGenerator; diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java index f0f752f596d4..25e34fc3a7d8 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java @@ -19,6 +19,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import jakarta.activation.FileTypeMap; import jakarta.activation.MimetypesFileTypeMap; @@ -178,4 +179,9 @@ public String getContentType(String fileName) { return getFileTypeMap().getContentType(fileName); } + // @Override - on Activation 2.2 + public String getContentType(Path path) { + return getFileTypeMap().getContentType(path.getFileName().toString()); + } + } diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java index b91db3658e82..1248b464e7a7 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/AdaptableJobFactory.java @@ -28,7 +28,7 @@ * {@link JobFactory} implementation that supports {@link java.lang.Runnable} * objects as well as standard Quartz {@link org.quartz.Job} instances. * - *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + *

    Compatible with Quartz 2.1.4 and higher. * * @author Juergen Hoeller * @since 2.0 diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java index 05bb23e2c964..ee233e7425c1 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java @@ -65,7 +65,7 @@ * You need to implement your own Quartz Job as a thin wrapper for each case * where you want a persistent job to delegate to a specific service method. * - *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + *

    Compatible with Quartz 2.1.4 and higher. * * @author Juergen Hoeller * @author Alef Arendsen diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java index dbf2627b631f..71c449b6048e 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java @@ -51,7 +51,7 @@ *

    For concrete usage, check out the {@link SchedulerFactoryBean} and * {@link SchedulerAccessorBean} classes. * - *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + *

    Compatible with Quartz 2.1.4 and higher. * * @author Juergen Hoeller * @author Stephane Nicoll diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java index 1286619df205..74b610569ee7 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java @@ -31,7 +31,7 @@ * Spring bean-style class for accessing a Quartz Scheduler, i.e. for registering jobs, * triggers and listeners on a given {@link org.quartz.Scheduler} instance. * - *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + *

    Compatible with Quartz 2.1.4 and higher. * * @author Juergen Hoeller * @since 2.5.6 diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java index 69c19d04a6d5..b9168297859b 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java @@ -80,7 +80,7 @@ * automatically apply to Scheduler operations performed within those scopes. * Alternatively, you may add transactional advice for the Scheduler itself. * - *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + *

    Compatible with Quartz 2.1.4 and higher. * * @author Juergen Hoeller * @since 18.02.2004 @@ -446,8 +446,7 @@ public void setStartupDelay(int startupDelay) { * Scheduler is usually exclusively intended for access within the Spring context. *

    Switch this flag to "true" in order to expose the Scheduler globally. * This is not recommended unless you have an existing Spring application that - * relies on this behavior. Note that such global exposure was the accidental - * default in earlier Spring versions; this has been fixed as of Spring 2.5.6. + * relies on this behavior. */ public void setExposeSchedulerInRepository(boolean exposeSchedulerInRepository) { this.exposeSchedulerInRepository = exposeSchedulerInRepository; diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java index e0cf70708033..0a913734682c 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java @@ -36,7 +36,7 @@ * as bean property values. If no matching bean property is found, the entry * is by default simply ignored. This is analogous to QuartzJobBean's behavior. * - *

    Compatible with Quartz 2.1.4 and higher, as of Spring 4.1. + *

    Compatible with Quartz 2.1.4 and higher. * * @author Juergen Hoeller * @since 2.0 diff --git a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java index 87adeaed3e5c..7ab4a8bd9487 100644 --- a/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java +++ b/spring-context-support/src/test/java/org/springframework/scheduling/quartz/QuartzSupportTests.java @@ -391,7 +391,6 @@ void schedulerWithHsqlDataSource() { try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) { JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class)); assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse(); - ctx.stop(); ctx.restart(); } } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java index 2437b5bf6d9f..149b5add4f1e 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java @@ -50,13 +50,17 @@ public abstract class AbstractCachingConfiguration implements ImportAware { protected @Nullable AnnotationAttributes enableCaching; - protected @Nullable Supplier<@Nullable CacheManager> cacheManager; + @SuppressWarnings("NullAway.Init") + protected Supplier<@Nullable CacheManager> cacheManager; - protected @Nullable Supplier<@Nullable CacheResolver> cacheResolver; + @SuppressWarnings("NullAway.Init") + protected Supplier<@Nullable CacheResolver> cacheResolver; - protected @Nullable Supplier<@Nullable KeyGenerator> keyGenerator; + @SuppressWarnings("NullAway.Init") + protected Supplier<@Nullable KeyGenerator> keyGenerator; - protected @Nullable Supplier<@Nullable CacheErrorHandler> errorHandler; + @SuppressWarnings("NullAway.Init") + protected Supplier<@Nullable CacheErrorHandler> errorHandler; @Override @@ -101,7 +105,7 @@ protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerS protected static class CachingConfigurerSupplier { - private final SingletonSupplier supplier; + private final SingletonSupplier<@Nullable CachingConfigurer> supplier; public CachingConfigurerSupplier(Supplier<@Nullable CachingConfigurer> supplier) { this.supplier = SingletonSupplier.ofNullable(supplier); diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java index 4a66f8e3cde2..666865cb47e5 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java @@ -50,14 +50,14 @@ public class CachingConfigurationSelector extends AdviceModeImportSelector result = new ArrayList<>(3); result.add(AutoProxyRegistrar.class.getName()); result.add(ProxyCachingConfiguration.class.getName()); - if (jsr107Present && jcacheImplPresent) { + if (JSR_107_PRESENT && JCACHE_IMPL_PRESENT) { result.add(PROXY_JCACHE_CONFIGURATION_CLASS); } return StringUtils.toStringArray(result); @@ -95,7 +95,7 @@ private String[] getProxyImports() { private String[] getAspectJImports() { List result = new ArrayList<>(2); result.add(CACHE_ASPECT_CONFIGURATION_CLASS_NAME); - if (jsr107Present && jcacheImplPresent) { + if (JSR_107_PRESENT && JCACHE_IMPL_PRESENT) { result.add(JCACHE_ASPECT_CONFIGURATION_CLASS_NAME); } return StringUtils.toStringArray(result); diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java index fea24a03fc9d..37ffbd4149da 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java @@ -24,14 +24,14 @@ import org.springframework.cache.interceptor.KeyGenerator; /** - * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration - * Configuration} classes annotated with @{@link EnableCaching} that wish or need to specify - * explicitly how caches are resolved and how keys are generated for annotation-driven - * cache management. + * Interface to be implemented for explicitly specifying how caches are resolved + * and how keys are generated for annotation-driven cache management. * - *

    See @{@link EnableCaching} for general examples and context; see - * {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()}, and - * {@link #errorHandler()} for detailed instructions. + *

    Typically implemented by @{@link org.springframework.context.annotation.Configuration + * Configuration} classes annotated with @{@link EnableCaching}. + * See @{@link EnableCaching} for general examples and context; see + * {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()}, + * and {@link #errorHandler()} for detailed instructions. * *

    NOTE: A {@code CachingConfigurer} will get initialized early. * Do not inject common dependencies into autowired fields directly; instead, consider diff --git a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java index 8f9934591238..d182c56ef4ef 100644 --- a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java @@ -61,14 +61,14 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser private static final String JCACHE_ASPECT_CLASS_NAME = "org.springframework.cache.aspectj.JCacheCacheAspect"; - private static final boolean jsr107Present; + private static final boolean JSR_107_PRESENT; - private static final boolean jcacheImplPresent; + private static final boolean JCACHE_IMPL_PRESENT; static { ClassLoader classLoader = AnnotationDrivenCacheBeanDefinitionParser.class.getClassLoader(); - jsr107Present = ClassUtils.isPresent("javax.cache.Cache", classLoader); - jcacheImplPresent = ClassUtils.isPresent( + JSR_107_PRESENT = ClassUtils.isPresent("javax.cache.Cache", classLoader); + JCACHE_IMPL_PRESENT = ClassUtils.isPresent( "org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource", classLoader); } @@ -95,7 +95,7 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser private void registerCacheAspect(Element element, ParserContext parserContext) { SpringCachingConfigurer.registerCacheAspect(element, parserContext); - if (jsr107Present && jcacheImplPresent) { + if (JSR_107_PRESENT && JCACHE_IMPL_PRESENT) { JCacheCachingConfigurer.registerCacheAspect(element, parserContext); } } @@ -103,7 +103,7 @@ private void registerCacheAspect(Element element, ParserContext parserContext) { private void registerCacheAdvisor(Element element, ParserContext parserContext) { AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); SpringCachingConfigurer.registerCacheAdvisor(element, parserContext); - if (jsr107Present && jcacheImplPresent) { + if (JSR_107_PRESENT && JCACHE_IMPL_PRESENT) { JCacheCachingConfigurer.registerCacheAdvisor(element, parserContext); } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index 166b7b7da8f6..58cde28f9fc4 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -28,6 +28,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.Aware; import org.springframework.core.MethodClassKey; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -136,6 +137,10 @@ protected Object getCacheKey(Method method, @Nullable Class targetClass) { if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } + // Skip methods declared on BeanFactoryAware and co. + if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) { + return null; + } // The method may be on an interface, but we need metadata from the target class. // If the target class is null, the method will be unchanged. diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index e72ced10c1ed..8fd02b97a749 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -114,10 +115,10 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker */ public static final String IGNORE_REACTIVESTREAMS_PROPERTY_NAME = "spring.cache.reactivestreams.ignore"; - private static final boolean shouldIgnoreReactiveStreams = + private static final boolean SHOULD_IGNORE_REACTIVE_STREAMS = SpringProperties.getFlag(IGNORE_REACTIVESTREAMS_PROPERTY_NAME); - private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent( "org.reactivestreams.Publisher", CacheAspectSupport.class.getClassLoader()); @@ -145,7 +146,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker protected CacheAspectSupport() { this.reactiveCachingHandler = - (reactiveStreamsPresent && !shouldIgnoreReactiveStreams ? new ReactiveCachingHandler() : null); + (REACTIVE_STREAMS_PRESENT && !SHOULD_IGNORE_REACTIVE_STREAMS ? new ReactiveCachingHandler() : null); } @@ -1170,7 +1171,7 @@ private class ReactiveCachingHandler { if (invokeFailure.get()) { return Mono.error(ex); } - return (Mono) invokeOperation(invoker); + return (Mono) Objects.requireNonNull(invokeOperation(invoker)); } catch (RuntimeException exception) { return Mono.error(exception); @@ -1201,8 +1202,8 @@ private class ReactiveCachingHandler { } if (adapter.isMultiValue()) { return adapter.fromPublisher(Flux.from(Mono.fromFuture(cachedFuture)) - .switchIfEmpty(Flux.defer(() -> (Flux) evaluate(null, invoker, method, contexts))) - .flatMap(v -> evaluate(valueToFlux(v, contexts), invoker, method, contexts)) + .switchIfEmpty(Flux.defer(() -> (Flux) Objects.requireNonNull(evaluate(null, invoker, method, contexts)))) + .flatMap(v -> Objects.requireNonNull(evaluate(valueToFlux(v, contexts), invoker, method, contexts))) .onErrorResume(RuntimeException.class, ex -> { try { getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); @@ -1216,8 +1217,8 @@ private class ReactiveCachingHandler { } else { return adapter.fromPublisher(Mono.fromFuture(cachedFuture) - .switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts))) - .flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts)) + .switchIfEmpty(Mono.defer(() -> (Mono) Objects.requireNonNull(evaluate(null, invoker, method, contexts)))) + .flatMap(v -> Objects.requireNonNull(evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts))) .onErrorResume(RuntimeException.class, ex -> { try { getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key); diff --git a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java index 6f56d8a6e519..6bef23396300 100644 --- a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java @@ -17,7 +17,6 @@ package org.springframework.context; import java.io.Closeable; -import java.util.concurrent.Executor; import org.jspecify.annotations.Nullable; @@ -25,7 +24,6 @@ import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; import org.springframework.core.io.ProtocolResolver; import org.springframework.core.metrics.ApplicationStartup; @@ -47,8 +45,8 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable { /** - * Any number of these characters are considered delimiters between - * multiple context config paths in a single String value. + * Any number of these characters are considered delimiters between multiple + * context config paths in a single {@code String} value: {@value}. * @see org.springframework.context.support.AbstractXmlApplicationContext#setConfigLocation * @see org.springframework.web.context.ContextLoader#CONFIG_LOCATION_PARAM * @see org.springframework.web.servlet.FrameworkServlet#setContextConfigLocation @@ -56,8 +54,9 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life String CONFIG_LOCATION_DELIMITERS = ",; \t\n"; /** - * The name of the {@link Executor bootstrap executor} bean in the context. - * If none is supplied, no background bootstrapping will be active. + * The name of the {@linkplain java.util.concurrent.Executor bootstrap executor} + * bean in the context: {@value}. + *

    If none is supplied, no background bootstrapping will be active. * @since 6.2 * @see java.util.concurrent.Executor * @see org.springframework.core.task.TaskExecutor @@ -66,48 +65,50 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life String BOOTSTRAP_EXECUTOR_BEAN_NAME = "bootstrapExecutor"; /** - * Name of the ConversionService bean in the factory. - * If none is supplied, default conversion rules apply. + * Name of the {@code ConversionService} bean in the factory: {@value}. + *

    If none is supplied, default conversion rules apply. * @since 3.0 * @see org.springframework.core.convert.ConversionService */ String CONVERSION_SERVICE_BEAN_NAME = "conversionService"; /** - * Name of the LoadTimeWeaver bean in the factory. If such a bean is supplied, - * the context will use a temporary ClassLoader for type matching, in order - * to allow the LoadTimeWeaver to process all actual bean classes. + * Name of the {@code LoadTimeWeaver} bean in the factory: {@value}. + *

    If such a bean is supplied, the context will use a temporary {@link ClassLoader} + * for type matching, in order to allow the {@code LoadTimeWeaver} to process + * all actual bean classes. * @since 2.5 * @see org.springframework.instrument.classloading.LoadTimeWeaver */ String LOAD_TIME_WEAVER_BEAN_NAME = "loadTimeWeaver"; /** - * Name of the {@link Environment} bean in the factory. + * Name of the {@link org.springframework.core.env.Environment Environment} + * bean in the factory: {@value}. * @since 3.1 */ String ENVIRONMENT_BEAN_NAME = "environment"; /** - * Name of the System properties bean in the factory. + * Name of the JVM System properties bean in the factory: {@value}. * @see java.lang.System#getProperties() */ String SYSTEM_PROPERTIES_BEAN_NAME = "systemProperties"; /** - * Name of the System environment bean in the factory. + * Name of the Operating System environment bean in the factory: {@value}. * @see java.lang.System#getenv() */ String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment"; /** - * Name of the {@link ApplicationStartup} bean in the factory. + * Name of the {@link ApplicationStartup} bean in the factory: {@value}. * @since 5.3 */ String APPLICATION_STARTUP_BEAN_NAME = "applicationStartup"; /** - * {@link Thread#getName() Name} of the {@linkplain #registerShutdownHook() + * {@linkplain Thread#getName() Name} of the {@linkplain #registerShutdownHook() * shutdown hook} thread: {@value}. * @since 5.2 * @see #registerShutdownHook() @@ -116,7 +117,7 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life /** - * Set the unique id of this application context. + * Set the unique ID of this application context. * @since 3.0 */ void setId(String id); @@ -221,16 +222,27 @@ public interface ConfigurableApplicationContext extends ApplicationContext, Life 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 - * 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). * @since 7.0 - * @see #stop() + * @see #pause() + * @see #start() * @see SmartLifecycle#isAutoStartup() */ 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 * on JVM shutdown unless it has already been closed at that time. diff --git a/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java index d9d4ea446fb6..c2e39a84d3da 100644 --- a/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/LifecycleProcessor.java @@ -44,6 +44,15 @@ default void onRestart() { 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 * before destruction. diff --git a/spring-context/src/main/java/org/springframework/context/MessageSource.java b/spring-context/src/main/java/org/springframework/context/MessageSource.java index 09b839a1fbc3..f5194bf36401 100644 --- a/spring-context/src/main/java/org/springframework/context/MessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/MessageSource.java @@ -54,7 +54,7 @@ public interface MessageSource { * @see #getMessage(MessageSourceResolvable, Locale) * @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. @@ -70,7 +70,7 @@ public interface MessageSource { * @see #getMessage(MessageSourceResolvable, Locale) * @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 @@ -90,6 +90,6 @@ public interface MessageSource { * @see MessageSourceResolvable#getDefaultMessage() * @see java.text.MessageFormat */ - String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException; + String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException; } diff --git a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java index ac2039cae684..cf4c43bd2643 100644 --- a/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java +++ b/spring-context/src/main/java/org/springframework/context/SmartLifecycle.java @@ -85,7 +85,7 @@ public interface SmartLifecycle extends Lifecycle, Phased { /** * Returns {@code true} if this {@code Lifecycle} component should get * started automatically by the container at the time that the containing - * {@link ApplicationContext} gets refreshed. + * {@link ApplicationContext} gets refreshed or restarted. *

    A value of {@code false} indicates that the component is intended to * be started through an explicit {@link #start()} call instead, analogous * to a plain {@link Lifecycle} implementation. @@ -93,12 +93,35 @@ public interface SmartLifecycle extends Lifecycle, Phased { * @see #start() * @see #getPhase() * @see LifecycleProcessor#onRefresh() + * @see LifecycleProcessor#onRestart() * @see ConfigurableApplicationContext#refresh() + * @see ConfigurableApplicationContext#restart() */ default boolean isAutoStartup() { 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. + *

    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. + *

    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. *

    The provided callback is used by the {@link LifecycleProcessor} to support diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AdviceMode.java b/spring-context/src/main/java/org/springframework/context/annotation/AdviceMode.java index 2f33ebe3af2a..ac06381da6af 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AdviceMode.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AdviceMode.java @@ -17,19 +17,18 @@ package org.springframework.context.annotation; /** - * Enumeration used to determine whether JDK proxy-based or + * Enumeration used to determine whether JDK/CGLIB proxy-based or * AspectJ weaving-based advice should be applied. * * @author Chris Beams * @since 3.1 - * @see org.springframework.scheduling.annotation.EnableAsync#mode() * @see org.springframework.scheduling.annotation.AsyncConfigurationSelector#selectImports - * @see org.springframework.transaction.annotation.EnableTransactionManagement#mode() + * @see org.springframework.scheduling.annotation.EnableAsync#mode() */ public enum AdviceMode { /** - * JDK proxy-based advice. + * JDK/CGLIB proxy-based advice. */ PROXY, diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java index 752e74819744..8bdf03e34fac 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java @@ -66,9 +66,7 @@ public class AnnotationConfigApplicationContext extends GenericApplicationContex * through {@link #register} calls and then manually {@linkplain #refresh refreshed}. */ public AnnotationConfigApplicationContext() { - StartupStep createAnnotatedBeanDefReader = getApplicationStartup().start("spring.context.annotated-bean-reader.create"); this.reader = new AnnotatedBeanDefinitionReader(this); - createAnnotatedBeanDefReader.end(); this.scanner = new ClassPathBeanDefinitionScanner(this); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index bc83fecad7ba..33e020f1546e 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -22,6 +22,7 @@ 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.AutowiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.config.BeanDefinition; @@ -115,10 +116,10 @@ public abstract class AnnotationConfigUtils { private static final ClassLoader classLoader = AnnotationConfigUtils.class.getClassLoader(); - private static final boolean jakartaAnnotationsPresent = + private static final boolean JAKARTA_ANNOTATIONS_PRESENT = ClassUtils.isPresent("jakarta.annotation.PostConstruct", classLoader); - private static final boolean jpaPresent = + private static final boolean JPA_PRESENT = ClassUtils.isPresent("jakarta.persistence.EntityManagerFactory", classLoader) && ClassUtils.isPresent(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, classLoader); @@ -167,14 +168,14 @@ public static Set registerAnnotationConfigProcessors( } // Check for Jakarta Annotations support, and if present add the CommonAnnotationBeanPostProcessor. - if (jakartaAnnotationsPresent && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) { + if (JAKARTA_ANNOTATIONS_PRESENT && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class); def.setSource(source); beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); } // Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor. - if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) { + if (JPA_PRESENT && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(); try { def.setBeanClass(ClassUtils.forName(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, @@ -258,6 +259,20 @@ else if (abd.getMetadata() != metadata) { if (description != null) { 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( diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java index 4985d68c6e88..808aa64d11a3 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java @@ -45,17 +45,15 @@ public static boolean isBeanAnnotated(Method method) { public static String determineBeanNameFor(Method beanMethod, ConfigurableBeanFactory beanFactory) { String beanName = retrieveBeanNameFor(beanMethod); - if (!beanName.isEmpty()) { - return beanName; + if (beanFactory.getSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR) + instanceof ConfigurationBeanNameGenerator cbng) { + return cbng.deriveBeanName(MethodMetadata.introspect(beanMethod), (!beanName.isEmpty() ? beanName : null)); } - return (beanFactory.getSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR) - instanceof ConfigurationBeanNameGenerator cbng ? - cbng.deriveBeanName(MethodMetadata.introspect(beanMethod)) : beanMethod.getName()); + return determineBeanNameFrom(beanName, beanMethod); } public static String determineBeanNameFor(Method beanMethod) { - String beanName = retrieveBeanNameFor(beanMethod); - return (!beanName.isEmpty() ? beanName : beanMethod.getName()); + return determineBeanNameFrom(retrieveBeanNameFor(beanMethod), beanMethod); } private static String retrieveBeanNameFor(Method beanMethod) { @@ -77,6 +75,10 @@ private static String retrieveBeanNameFor(Method beanMethod) { return beanName; } + private static String determineBeanNameFrom(String derivedBeanName, Method beanMethod) { + return (!derivedBeanName.isEmpty() ? derivedBeanName : beanMethod.getName()); + } + public static boolean isScopedProxy(Method beanMethod) { Boolean scopedProxy = scopedProxyCache.get(beanMethod); if (scopedProxy == null) { @@ -88,4 +90,9 @@ public static boolean isScopedProxy(Method beanMethod) { return scopedProxy; } + static void clearCaches() { + scopedProxyCache.clear(); + beanNameCache.clear(); + } + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index ab4ebefaaea8..442c4114adb9 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -90,7 +90,6 @@ * @see ScannedGenericBeanDefinition * @see CandidateComponentsIndex */ -@SuppressWarnings("removal") // components index public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware { static final String DEFAULT_RESOURCE_PATTERN = "**/*.class"; @@ -312,11 +311,14 @@ public final MetadataReaderFactory getMetadataReaderFactory() { */ public Set findCandidateComponents(String basePackage) { if (this.componentsIndex != null && indexSupportsIncludeFilters()) { - return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); - } - else { - return scanCandidateComponents(basePackage); + if (this.componentsIndex.hasScannedPackage(basePackage)) { + return addCandidateComponentsFromIndex(this.componentsIndex, basePackage); + } + else { + this.componentsIndex.registerScan(basePackage); + } } + return scanCandidateComponents(basePackage); } /** @@ -339,14 +341,13 @@ private boolean indexSupportsIncludeFilters() { * @param filter the filter to check * @return whether the index supports this include filter * @since 5.0 + * @see #registerCandidateTypeForIncludeFilter(String, TypeFilter) * @see #extractStereotype(TypeFilter) */ private boolean indexSupportsIncludeFilter(TypeFilter filter) { if (filter instanceof AnnotationTypeFilter annotationTypeFilter) { Class annotationType = annotationTypeFilter.getAnnotationType(); - return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) || - annotationType.getName().startsWith("jakarta.") || - annotationType.getName().startsWith("javax.")); + return isStereotypeAnnotationForIndex(annotationType); } if (filter instanceof AssignableTypeFilter assignableTypeFilter) { Class target = assignableTypeFilter.getTargetType(); @@ -355,6 +356,28 @@ private boolean indexSupportsIncludeFilter(TypeFilter filter) { return false; } + /** + * Register the given class as a candidate type with the runtime-populated index, if any. + * @param className the fully-qualified class name of the candidate type + * @param filter the include filter to introspect for the associated stereotype + */ + private void registerCandidateTypeForIncludeFilter(String className, TypeFilter filter) { + if (this.componentsIndex != null) { + if (filter instanceof AnnotationTypeFilter annotationTypeFilter) { + Class annotationType = annotationTypeFilter.getAnnotationType(); + if (isStereotypeAnnotationForIndex(annotationType)) { + this.componentsIndex.registerCandidateType(className, annotationType.getName()); + } + } + else if (filter instanceof AssignableTypeFilter assignableTypeFilter) { + Class target = assignableTypeFilter.getTargetType(); + if (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target)) { + this.componentsIndex.registerCandidateType(className, target.getName()); + } + } + } + } + /** * Extract the stereotype to use for the specified compatible filter. * @param filter the filter to handle @@ -372,6 +395,11 @@ private boolean indexSupportsIncludeFilter(TypeFilter filter) { return null; } + private boolean isStereotypeAnnotationForIndex(Class annotationType) { + return (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType) || + annotationType.getName().startsWith("jakarta.")); + } + private Set addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) { Set candidates = new LinkedHashSet<>(); try { @@ -418,9 +446,9 @@ private Set addCandidateComponentsFromIndex(CandidateComponentsI private Set scanCandidateComponents(String basePackage) { Set candidates = new LinkedHashSet<>(); try { - String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + + String packageSearchPattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern; - Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath); + Resource[] resources = getResourcePatternResolver().getResources(packageSearchPattern); boolean traceEnabled = logger.isTraceEnabled(); boolean debugEnabled = logger.isDebugEnabled(); for (Resource resource : resources) { @@ -503,13 +531,14 @@ protected String resolveBasePackage(String basePackage) { * @return whether the class qualifies as a candidate component */ protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException { - for (TypeFilter tf : this.excludeFilters) { - if (tf.match(metadataReader, getMetadataReaderFactory())) { + for (TypeFilter filter : this.excludeFilters) { + if (filter.match(metadataReader, getMetadataReaderFactory())) { return false; } } - for (TypeFilter tf : this.includeFilters) { - if (tf.match(metadataReader, getMetadataReaderFactory())) { + for (TypeFilter filter : this.includeFilters) { + if (filter.match(metadataReader, getMetadataReaderFactory())) { + registerCandidateTypeForIncludeFilter(metadataReader.getClassMetadata().getClassName(), filter); return isConditionMatch(metadataReader); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index cbfecabd6b93..b36de992d5b3 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -142,24 +142,24 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable { // Defensive reference to JNDI API for JDK 9+ (optional java.naming module) - private static final boolean jndiPresent = ClassUtils.isPresent( + private static final boolean JNDI_PRESENT = ClassUtils.isPresent( "javax.naming.InitialContext", CommonAnnotationBeanPostProcessor.class.getClassLoader()); - private static final Set> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(3); + private static final @Nullable Class JAKARTA_RESOURCE_TYPE; - private static final @Nullable Class jakartaResourceType; + private static final @Nullable Class EJB_ANNOTATION_TYPE; - private static final @Nullable Class ejbAnnotationType; + private static final Set> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(2); static { - jakartaResourceType = loadAnnotationType("jakarta.annotation.Resource"); - if (jakartaResourceType != null) { - resourceAnnotationTypes.add(jakartaResourceType); + JAKARTA_RESOURCE_TYPE = loadAnnotationType("jakarta.annotation.Resource"); + if (JAKARTA_RESOURCE_TYPE != null) { + resourceAnnotationTypes.add(JAKARTA_RESOURCE_TYPE); } - ejbAnnotationType = loadAnnotationType("jakarta.ejb.EJB"); - if (ejbAnnotationType != null) { - resourceAnnotationTypes.add(ejbAnnotationType); + EJB_ANNOTATION_TYPE = loadAnnotationType("jakarta.ejb.EJB"); + if (EJB_ANNOTATION_TYPE != null) { + resourceAnnotationTypes.add(EJB_ANNOTATION_TYPE); } } @@ -195,7 +195,7 @@ public CommonAnnotationBeanPostProcessor() { addDestroyAnnotationType(loadAnnotationType("jakarta.annotation.PreDestroy")); // java.naming module present on JDK 9+? - if (jndiPresent) { + if (JNDI_PRESENT) { this.jndiFactory = new SimpleJndiBeanFactory(); } } @@ -399,19 +399,19 @@ private InjectionMetadata buildResourceMetadata(Class clazz) { } List elements = new ArrayList<>(); - Class targetClass = clazz; + Class targetClass = ClassUtils.getUserClass(clazz); do { final List currElements = new ArrayList<>(); ReflectionUtils.doWithLocalFields(targetClass, field -> { - if (ejbAnnotationType != null && field.isAnnotationPresent(ejbAnnotationType)) { + if (EJB_ANNOTATION_TYPE != null && field.isAnnotationPresent(EJB_ANNOTATION_TYPE)) { if (Modifier.isStatic(field.getModifiers())) { throw new IllegalStateException("@EJB annotation is not supported on static fields"); } currElements.add(new EjbRefElement(field, field, null)); } - else if (jakartaResourceType != null && field.isAnnotationPresent(jakartaResourceType)) { + else if (JAKARTA_RESOURCE_TYPE != null && field.isAnnotationPresent(JAKARTA_RESOURCE_TYPE)) { if (Modifier.isStatic(field.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static fields"); } @@ -422,24 +422,23 @@ else if (jakartaResourceType != null && field.isAnnotationPresent(jakartaResourc }); ReflectionUtils.doWithLocalMethods(targetClass, method -> { - Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); - if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { + if (method.isBridge()) { return; } - if (ejbAnnotationType != null && bridgedMethod.isAnnotationPresent(ejbAnnotationType)) { - if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (EJB_ANNOTATION_TYPE != null && method.isAnnotationPresent(EJB_ANNOTATION_TYPE)) { + if (method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@EJB annotation is not supported on static methods"); } if (method.getParameterCount() != 1) { throw new IllegalStateException("@EJB annotation requires a single-arg method: " + method); } - PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); - currElements.add(new EjbRefElement(method, bridgedMethod, pd)); + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz); + currElements.add(new EjbRefElement(method, method, pd)); } } - else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakartaResourceType)) { - if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + else if (JAKARTA_RESOURCE_TYPE != null && method.isAnnotationPresent(JAKARTA_RESOURCE_TYPE)) { + if (method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalStateException("@Resource annotation is not supported on static methods"); } @@ -448,8 +447,8 @@ else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakart throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method); } if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) { - PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); - currElements.add(new ResourceElement(method, bridgedMethod, pd)); + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz); + currElements.add(new ResourceElement(method, method, pd)); } } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java index a6e4f7f873c0..621fd708e8b0 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -74,8 +74,8 @@ /** * Alias for {@link #basePackages}. *

    Allows for more concise annotation declarations if no other attributes - * are needed — for example, {@code @ComponentScan("org.my.pkg")} - * instead of {@code @ComponentScan(basePackages = "org.my.pkg")}. + * are needed — for example, {@code @ComponentScan("org.example")} + * instead of {@code @ComponentScan(basePackages = "org.example")}. */ @AliasFor("basePackages") String[] value() default {}; @@ -84,8 +84,16 @@ * Base packages to scan for annotated components. *

    {@link #value} is an alias for (and mutually exclusive with) this * attribute. + *

    Supports {@code ${...}} placeholders which are resolved against the + * {@link org.springframework.core.env.Environment Environment} as well as + * Ant-style package patterns — for example, {@code "org.example.**"}. + *

    Multiple packages or patterns may be specified, either separately or + * within a single {@code String} — for example, + * {@code {"org.example.config", "org.example.service.**"}} or + * {@code "org.example.config, org.example.service.**"}. *

    Use {@link #basePackageClasses} for a type-safe alternative to * String-based package names. + * @see org.springframework.context.ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS */ @AliasFor("value") String[] basePackages() default {}; diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationBeanNameGenerator.java index 52f51affd43f..2787c2b82455 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationBeanNameGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationBeanNameGenerator.java @@ -16,6 +16,8 @@ package org.springframework.context.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.core.type.MethodMetadata; @@ -23,10 +25,10 @@ * Extended variant of {@link BeanNameGenerator} for * {@link Configuration @Configuration} class purposes, not only covering * bean name generation for component and configuration classes themselves - * but also for {@link Bean @Bean} methods without a {@link Bean#name() name} - * attribute specified on the annotation itself. + * but also for {@link Bean @Bean} methods. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 7.0 * @see AnnotationConfigApplicationContext#setBeanNameGenerator * @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR @@ -35,10 +37,11 @@ public interface ConfigurationBeanNameGenerator extends BeanNameGenerator { /** * Derive a default bean name for the given {@link Bean @Bean} method, - * in the absence of a {@link Bean#name() name} attribute specified. + * providing the {@link Bean#name() name} attribute specified. * @param beanMethod the method metadata for the {@link Bean @Bean} method + * @param beanName the {@link Bean#name() name} attribute or {@code null} if non is specified * @return the default bean name to use */ - String deriveBeanName(MethodMetadata beanMethod); + String deriveBeanName(MethodMetadata beanMethod, @Nullable String beanName); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 4bda6a0c19b9..c6b5841a9ae6 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -36,6 +36,8 @@ import org.springframework.core.type.classreading.MetadataReader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Represents a user-defined {@link Configuration @Configuration} class. @@ -66,7 +68,7 @@ final class ConfigurationClass { private final Map> importedResources = new LinkedHashMap<>(); - private final Map beanRegistrars = new LinkedHashMap<>(); + private final MultiValueMap beanRegistrars = new LinkedMultiValueMap<>(); private final Map importBeanDefinitionRegistrars = new LinkedHashMap<>(); @@ -224,10 +226,10 @@ Map> getImportedResources() { } void addBeanRegistrar(String sourceClassName, BeanRegistrar beanRegistrar) { - this.beanRegistrars.put(sourceClassName, beanRegistrar); + this.beanRegistrars.add(sourceClassName, beanRegistrar); } - public Map getBeanRegistrars() { + public MultiValueMap getBeanRegistrars() { return this.beanRegistrars; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java index 09bdae4bb8ad..86784364c77f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -56,6 +56,7 @@ import org.springframework.core.type.StandardMethodMetadata; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -161,9 +162,17 @@ private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationCl ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef); configBeanDef.setScope(scopeMetadata.getScopeName()); - String configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry); - AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata); + String configBeanName; + try { + configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry); + } + catch (IllegalArgumentException ex) { + throw new IllegalStateException("Failed to generate bean name for imported class '" + + configClass.getMetadata().getClassName() + "'", ex); + } + + AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata); BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); this.registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition()); @@ -197,22 +206,16 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { // Consider name and any aliases. String[] explicitNames = bean.getStringArray("name"); - String beanName; - String localBeanName; - if (explicitNames.length > 0 && StringUtils.hasText(explicitNames[0])) { - beanName = explicitNames[0]; - localBeanName = beanName; + String beanName = (explicitNames.length > 0 && StringUtils.hasText(explicitNames[0])) ? explicitNames[0] : null; + String localBeanName = defaultBeanName(beanName, methodName); + beanName = (this.importBeanNameGenerator instanceof ConfigurationBeanNameGenerator cbng ? + cbng.deriveBeanName(metadata, beanName) : defaultBeanName(beanName, methodName)); + if (explicitNames.length > 0) { // Register aliases even when overridden below. for (int i = 1; i < explicitNames.length; i++) { this.registry.registerAlias(beanName, explicitNames[i]); } } - else { - // Default bean name derived from method name. - beanName = (this.importBeanNameGenerator instanceof ConfigurationBeanNameGenerator cbng ? - cbng.deriveBeanName(metadata) : methodName); - localBeanName = methodName; - } ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, localBeanName); @@ -306,6 +309,10 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { this.registry.registerBeanDefinition(beanName, beanDefToRegister); } + private static String defaultBeanName(@Nullable String beanName, String methodName) { + return (beanName != null ? beanName : methodName); + } + @SuppressWarnings("NullAway") // Reflection private boolean isOverriddenByExistingDefinition( BeanMethod beanMethod, String beanName, ConfigurationClassBeanDefinition newBeanDef) { @@ -415,13 +422,13 @@ private void loadBeanDefinitionsFromImportBeanDefinitionRegistrars( registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator)); } - private void loadBeanDefinitionsFromBeanRegistrars(Map registrars) { + private void loadBeanDefinitionsFromBeanRegistrars(MultiValueMap registrars) { if (!(this.registry instanceof ListableBeanFactory beanFactory)) { throw new IllegalStateException("Cannot support bean registrars since " + this.registry.getClass().getName() + " does not implement ListableBeanFactory"); } - registrars.values().forEach(registrar -> registrar.register(new BeanRegistryAdapter( - this.registry, beanFactory, this.environment, registrar.getClass()), this.environment)); + registrars.values().forEach(registrarList -> registrarList.forEach(registrar -> registrar.register(new BeanRegistryAdapter( + this.registry, beanFactory, this.environment, registrar.getClass()), this.environment))); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 91812cc9e0e4..e9825b369b36 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -199,7 +199,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private List propertySourceDescriptors = Collections.emptyList(); - private final Map beanRegistrars = new LinkedHashMap<>(); + private final MultiValueMap beanRegistrars = new LinkedMultiValueMap<>(); @Override @@ -453,7 +453,7 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. } this.reader.loadBeanDefinitions(configClasses); for (ConfigurationClass configClass : configClasses) { - this.beanRegistrars.putAll(configClass.getBeanRegistrars()); + this.beanRegistrars.addAll(configClass.getBeanRegistrars()); } alreadyParsed.addAll(configClasses); processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end(); @@ -857,13 +857,13 @@ private static class BeanRegistrarAotContribution implements BeanFactoryInitiali private static final String ENVIRONMENT_VARIABLE = "environment"; - private final Map beanRegistrars; + private final MultiValueMap beanRegistrars; private final ConfigurableListableBeanFactory beanFactory; private final AotServices aotProcessors; - public BeanRegistrarAotContribution(Map beanRegistrars, ConfigurableListableBeanFactory beanFactory) { + public BeanRegistrarAotContribution(MultiValueMap beanRegistrars, ConfigurableListableBeanFactory beanFactory) { this.beanRegistrars = beanRegistrars; this.beanFactory = beanFactory; this.aotProcessors = AotServices.factoriesAndBeans(this.beanFactory).load(BeanRegistrationAotProcessor.class); @@ -928,13 +928,6 @@ private void checkUnsupportedFeatures(AbstractBeanDefinition beanDefinition) { if (!beanDefinition.getQualifiers().isEmpty()) { throw new UnsupportedOperationException("AOT post processing of qualifiers is not supported yet with BeanRegistrar"); } - for (String attributeName : beanDefinition.attributeNames()) { - if (!attributeName.equals(AbstractBeanDefinition.ORDER_ATTRIBUTE) && - !attributeName.equals("aotProcessingIgnoreRegistration")) { - throw new UnsupportedOperationException("AOT post processing of attribute " + attributeName + - " is not supported yet with BeanRegistrar"); - } - } } private CodeBlock generateCustomizerMap() { @@ -948,28 +941,29 @@ private CodeBlock generateRegisterCode() { Builder code = CodeBlock.builder(); Builder metadataReaderFactoryCode = null; NameAllocator nameAllocator = new NameAllocator(); - for (Map.Entry beanRegistrarEntry : this.beanRegistrars.entrySet()) { - BeanRegistrar beanRegistrar = beanRegistrarEntry.getValue(); - String beanRegistrarName = nameAllocator.newName(StringUtils.uncapitalize(beanRegistrar.getClass().getSimpleName())); - code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass()); - if (beanRegistrar instanceof ImportAware) { - if (metadataReaderFactoryCode == null) { - metadataReaderFactoryCode = CodeBlock.builder(); - metadataReaderFactoryCode.addStatement("$T metadataReaderFactory = new $T()", - MetadataReaderFactory.class, CachingMetadataReaderFactory.class); + for (Map.Entry> beanRegistrarEntry : this.beanRegistrars.entrySet()) { + for (BeanRegistrar beanRegistrar : beanRegistrarEntry.getValue()) { + String beanRegistrarName = nameAllocator.newName(StringUtils.uncapitalize(beanRegistrar.getClass().getSimpleName())); + code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass()); + if (beanRegistrar instanceof ImportAware) { + if (metadataReaderFactoryCode == null) { + metadataReaderFactoryCode = CodeBlock.builder(); + metadataReaderFactoryCode.addStatement("$T metadataReaderFactory = new $T()", + MetadataReaderFactory.class, CachingMetadataReaderFactory.class); + } + code.beginControlFlow("try") + .addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())", + beanRegistrarName, beanRegistrarEntry.getKey()) + .nextControlFlow("catch ($T ex)", IOException.class) + .addStatement("throw new $T(\"Failed to read metadata for '$L'\", ex)", + IllegalStateException.class, beanRegistrarEntry.getKey()) + .endControlFlow(); } - code.beginControlFlow("try") - .addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())", - beanRegistrarName, beanRegistrarEntry.getKey()) - .nextControlFlow("catch ($T ex)", IOException.class) - .addStatement("throw new $T(\"Failed to read metadata for '$L'\", ex)", - IllegalStateException.class, beanRegistrarEntry.getKey()) - .endControlFlow(); + code.addStatement("$L.register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrarName, + BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, + BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrar.getClass(), + CUSTOMIZER_MAP_VARIABLE, ENVIRONMENT_VARIABLE); } - code.addStatement("$L.register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrarName, - BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, - BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrar.getClass(), - CUSTOMIZER_MAP_VARIABLE, ENVIRONMENT_VARIABLE); } return (metadataReaderFactoryCode == null ? code.build() : metadataReaderFactoryCode.add(code.build()).build()); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedConfigurationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedConfigurationBeanNameGenerator.java index 677d74dc11c6..68e88ebfab21 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedConfigurationBeanNameGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedConfigurationBeanNameGenerator.java @@ -16,6 +16,8 @@ package org.springframework.context.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.type.MethodMetadata; /** @@ -23,8 +25,8 @@ * {@link Configuration @Configuration} class purposes, not only enforcing * fully-qualified names for component and configuration classes themselves * but also fully-qualified default bean names ("className.methodName") for - * {@link Bean @Bean} methods. This only affects methods without an explicit - * {@link Bean#name() name} attribute specified. + * {@link Bean @Bean} methods. By default, this only affects methods without + * an explicit {@link Bean#name() name} attribute specified. * *

    This provides an alternative to the default bean name generation for * {@code @Bean} methods (which uses the plain method name), primarily for use @@ -54,8 +56,8 @@ public class FullyQualifiedConfigurationBeanNameGenerator extends FullyQualified @Override - public String deriveBeanName(MethodMetadata beanMethod) { - return beanMethod.getDeclaringClassName() + "." + beanMethod.getMethodName(); + public String deriveBeanName(MethodMetadata beanMethod, @Nullable String beanName) { + return (beanName != null ? beanName : beanMethod.getDeclaringClassName() + "." + beanMethod.getMethodName()); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ProxyType.java b/spring-context/src/main/java/org/springframework/context/annotation/ProxyType.java new file mode 100644 index 000000000000..e733b8b27efc --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/ProxyType.java @@ -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 all 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 + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Proxyable.java b/spring-context/src/main/java/org/springframework/context/annotation/Proxyable.java new file mode 100644 index 000000000000..623364ea2d3a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/Proxyable.java @@ -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. + * + *

    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. + *

    Only taken into account if {@link #value()} is not {@link ProxyType#TARGET_CLASS}. + */ + Class[] interfaces() default {}; + + +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java index c71da8589e77..b83bff6ef9a5 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ReflectiveScan.java @@ -51,9 +51,9 @@ * ignored. * * @author Stephane Nicoll + * @since 6.2 * @see Reflective @Reflective * @see RegisterReflection @RegisterReflection - * @since 6.2 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) diff --git a/spring-context/src/main/java/org/springframework/context/aot/AotApplicationContextInitializer.java b/spring-context/src/main/java/org/springframework/context/aot/AotApplicationContextInitializer.java index 4a290d9b502b..7a14224d8089 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/AotApplicationContextInitializer.java +++ b/spring-context/src/main/java/org/springframework/context/aot/AotApplicationContextInitializer.java @@ -63,13 +63,13 @@ static AotApplicationContextInitializ private static void initialize( C applicationContext, String... initializerClassNames) { + Log logger = LogFactory.getLog(AotApplicationContextInitializer.class); ClassLoader classLoader = applicationContext.getClassLoader(); logger.debug("Initializing ApplicationContext with AOT"); for (String initializerClassName : initializerClassNames) { logger.trace(LogMessage.format("Applying %s", initializerClassName)); - instantiateInitializer(initializerClassName, classLoader) - .initialize(applicationContext); + instantiateInitializer(initializerClassName, classLoader).initialize(applicationContext); } } diff --git a/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java index de0fe66eb341..ddfc13b5f38c 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java @@ -136,9 +136,7 @@ protected ClassNameGenerator createClassNameGenerator() { protected List getDefaultNativeImageArguments(String applicationClassName) { List args = new ArrayList<>(); args.add("-H:Class=" + applicationClassName); - args.add("--report-unsupported-elements-at-runtime"); args.add("--no-fallback"); - args.add("--install-exit-handlers"); return args; } diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index 37dcba7fb179..e2f7c2bec6a5 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -74,7 +74,7 @@ */ public class ApplicationListenerMethodAdapter implements GenericApplicationListener { - private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( + private static final boolean REACTIVE_STREAMS_PRESENT = ClassUtils.isPresent( "org.reactivestreams.Publisher", ApplicationListenerMethodAdapter.class.getClassLoader()); @@ -309,7 +309,7 @@ private boolean shouldHandle(ApplicationEvent event, @Nullable Object @Nullable } protected void handleResult(Object result) { - if (reactiveStreamsPresent && new ReactiveResultHandler().subscribeToPublisher(result)) { + if (REACTIVE_STREAMS_PRESENT && new ReactiveResultHandler().subscribeToPublisher(result)) { if (logger.isTraceEnabled()) { logger.trace("Adapted to reactive result: " + result); } diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextPausedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextPausedEvent.java new file mode 100644 index 000000000000..6fee1579918d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ContextPausedEvent.java @@ -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. + * + *

    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); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/ContextRestartedEvent.java b/spring-context/src/main/java/org/springframework/context/event/ContextRestartedEvent.java index e8165be8d08b..8ac44108917f 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ContextRestartedEvent.java +++ b/spring-context/src/main/java/org/springframework/context/event/ContextRestartedEvent.java @@ -17,6 +17,7 @@ package org.springframework.context.event; import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; /** * Event raised when an {@code ApplicationContext} gets restarted. @@ -26,8 +27,9 @@ * * @author Sam Brannen * @since 7.0 + * @see ConfigurableApplicationContext#restart() + * @see ContextPausedEvent * @see ContextStartedEvent - * @see ContextStoppedEvent */ @SuppressWarnings("serial") public class ContextRestartedEvent extends ContextStartedEvent { diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListener.java b/spring-context/src/main/java/org/springframework/context/event/EventListener.java index e08a2a81171c..0fc2a4d06bc1 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListener.java @@ -101,10 +101,9 @@ /** * The event classes that this listener handles. - *

    If this attribute is specified with a single value, the - * annotated method may optionally accept a single parameter. - * However, if this attribute is specified with multiple values, - * the annotated method must not declare any parameters. + *

    The annotated method may optionally accept a single parameter + * of the given event class, or of a common base class or interface + * for all given event classes. */ @AliasFor("value") Class[] classes() default {}; diff --git a/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java index 3d08f3345b0a..c8f19b0a1c86 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java @@ -16,18 +16,7 @@ package org.springframework.context.expression; -import java.util.Map; - -import org.jspecify.annotations.Nullable; - -import org.springframework.asm.MethodVisitor; -import org.springframework.expression.AccessException; -import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; -import org.springframework.expression.TypedValue; -import org.springframework.expression.spel.CodeFlow; -import org.springframework.expression.spel.CompilablePropertyAccessor; -import org.springframework.util.Assert; /** * SpEL {@link PropertyAccessor} that knows how to access the keys of a standard @@ -36,11 +25,10 @@ * @author Juergen Hoeller * @author Andy Clement * @since 3.0 + * @deprecated as of Spring Framework 7.0 in favor of {@link org.springframework.expression.spel.support.MapAccessor}. */ -public class MapAccessor implements CompilablePropertyAccessor { - - private final boolean allowWrite; - +@Deprecated(since = "7.0", forRemoval = true) +public class MapAccessor extends org.springframework.expression.spel.support.MapAccessor { /** * Create a new {@code MapAccessor} for reading as well as writing. @@ -57,88 +45,7 @@ public MapAccessor() { * @see #canWrite */ public MapAccessor(boolean allowWrite) { - this.allowWrite = allowWrite; - } - - - @Override - public Class[] getSpecificTargetClasses() { - return new Class[] {Map.class}; - } - - @Override - public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { - return (target instanceof Map map && map.containsKey(name)); - } - - @Override - public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { - Assert.state(target instanceof Map, "Target must be of type Map"); - Map map = (Map) target; - Object value = map.get(name); - if (value == null && !map.containsKey(name)) { - throw new MapAccessException(name); - } - return new TypedValue(value); - } - - @Override - public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { - return (this.allowWrite && target instanceof Map); - } - - @Override - @SuppressWarnings("unchecked") - public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) - throws AccessException { - - Assert.state(target instanceof Map, "Target must be of type Map"); - Map map = (Map) target; - map.put(name, newValue); - } - - @Override - public boolean isCompilable() { - return true; - } - - @Override - public Class getPropertyType() { - return Object.class; - } - - @Override - public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { - String descriptor = cf.lastDescriptor(); - if (descriptor == null || !descriptor.equals("Ljava/util/Map")) { - if (descriptor == null) { - cf.loadTarget(mv); - } - CodeFlow.insertCheckCast(mv, "Ljava/util/Map"); - } - mv.visitLdcInsn(propertyName); - mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true); - } - - - /** - * Exception thrown from {@code read} in order to reset a cached - * PropertyAccessor, allowing other accessors to have a try. - */ - @SuppressWarnings("serial") - private static class MapAccessException extends AccessException { - - private final String key; - - public MapAccessException(String key) { - super(""); - this.key = key; - } - - @Override - public String getMessage() { - return "Map does not contain a value for key '" + this.key + "'"; - } + super(allowWrite); } } diff --git a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java index 20f46c916913..798d954e577d 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java +++ b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java @@ -168,7 +168,7 @@ public void setExpressionParser(ExpressionParser expressionParser) { StandardEvaluationContext sec = new StandardEvaluationContext(bec); sec.addPropertyAccessor(new BeanExpressionContextAccessor()); sec.addPropertyAccessor(new BeanFactoryAccessor()); - sec.addPropertyAccessor(new MapAccessor()); + sec.addPropertyAccessor(new org.springframework.expression.spel.support.MapAccessor()); sec.addPropertyAccessor(new EnvironmentAccessor()); sec.setBeanResolver(new BeanFactoryResolver(beanFactory)); sec.setTypeLocator(new StandardTypeLocator(beanFactory.getBeanClassLoader())); diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java index 49c20023f82d..9b176cd85f9b 100644 --- a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndex.java @@ -17,6 +17,7 @@ package org.springframework.context.index; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Properties; import java.util.Set; @@ -28,7 +29,9 @@ import org.springframework.util.MultiValueMap; /** - * Provide access to the candidates that are defined in {@code META-INF/spring.components}. + * Provide access to the candidates that are defined in {@code META-INF/spring.components} + * component index files (see {@link #CandidateComponentsIndex(List)}) or registered + * programmatically (see {@link #CandidateComponentsIndex()}). * *

    An arbitrary number of stereotypes can be registered (and queried) on the index: a * typical example is the fully qualified name of an annotation that flags the class for @@ -41,37 +44,99 @@ * *

    The {@code type} is usually the fully qualified name of a class, though this is * not a rule. Similarly, the {@code stereotype} is usually the fully qualified name of - * a target type but it can be any marker really. + * an annotation type, but it can be any marker really. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 5.0 - * @deprecated as of 6.1, in favor of the AOT engine. */ -@Deprecated(since = "6.1", forRemoval = true) public class CandidateComponentsIndex { private static final AntPathMatcher pathMatcher = new AntPathMatcher("."); - private final MultiValueMap index; + private final Set registeredScans = new LinkedHashSet<>(); + private final MultiValueMap index = new LinkedMultiValueMap<>(); + + private final boolean complete; - CandidateComponentsIndex(List content) { - this.index = parseIndex(content); - } - private static MultiValueMap parseIndex(List content) { - MultiValueMap index = new LinkedMultiValueMap<>(); + /** + * Create a new index instance from parsed component index files. + */ + CandidateComponentsIndex(List content) { for (Properties entry : content) { entry.forEach((type, values) -> { String[] stereotypes = ((String) values).split(","); for (String stereotype : stereotypes) { - index.add(stereotype, new Entry((String) type)); + this.index.add(stereotype, new Entry((String) type)); } }); } - return index; + this.complete = true; + } + + /** + * Create a new index instance for programmatic population. + * @since 7.0 + * @see #registerScan(String...) + * @see #registerCandidateType(String, String...) + */ + public CandidateComponentsIndex() { + this.complete = false; + } + + + /** + * Programmatically register the given base packages (or base package patterns) + * as scanned. + * @since 7.0 + * @see #registerCandidateType(String, String...) + */ + public void registerScan(String... basePackages) { + Collections.addAll(this.registeredScans, basePackages); + } + + /** + * Return the registered base packages (or base package patterns). + * @since 7.0 + * @see #registerScan(String...) + */ + public Set getRegisteredScans() { + return this.registeredScans; + } + + /** + * Determine whether this index contains an entry for the given base package + * (or base package pattern). + * @since 7.0 + */ + public boolean hasScannedPackage(String packageName) { + return (this.complete || + this.registeredScans.stream().anyMatch(basePackage -> matchPackage(basePackage, packageName))); + } + + /** + * Programmatically register one or more stereotypes for the given candidate type. + *

    Note that the containing packages for candidates are not automatically + * considered scanned packages. Make sure to call {@link #registerScan(String...)} + * with the scan-specific base package accordingly. + * @since 7.0 + * @see #registerScan(String...) + */ + public void registerCandidateType(String type, String... stereotypes) { + for (String stereotype : stereotypes) { + this.index.add(stereotype, new Entry(type)); + } } + /** + * Return the registered stereotype packages (or base package patterns). + * @since 7.0 + */ + public Set getRegisteredStereotypes() { + return this.index.keySet(); + } /** * Return the candidate types that are associated with the specified stereotype. @@ -83,18 +148,28 @@ private static MultiValueMap parseIndex(List content) public Set getCandidateTypes(String basePackage, String stereotype) { List candidates = this.index.get(stereotype); if (candidates != null) { - return candidates.parallelStream() - .filter(t -> t.match(basePackage)) - .map(t -> t.type) + return candidates.stream() + .filter(entry -> entry.match(basePackage)) + .map(entry -> entry.type) .collect(Collectors.toSet()); } return Collections.emptySet(); } + private static boolean matchPackage(String basePackage, String packageName) { + if (pathMatcher.isPattern(basePackage)) { + return pathMatcher.match(basePackage, packageName); + } + else { + return packageName.equals(basePackage) || packageName.startsWith(basePackage + "."); + } + } + + private static class Entry { - private final String type; + final String type; private final String packageName; @@ -104,12 +179,7 @@ private static class Entry { } public boolean match(String basePackage) { - if (pathMatcher.isPattern(basePackage)) { - return pathMatcher.match(basePackage, this.packageName); - } - else { - return this.type.startsWith(basePackage); - } + return matchPackage(basePackage, this.packageName); } } diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java index 091bd33888f0..501a3c77da22 100644 --- a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java @@ -37,11 +37,9 @@ * Candidate components index loading mechanism for internal use within the framework. * * @author Stephane Nicoll + * @author Juergen Hoeller * @since 5.0 - * @deprecated as of 6.1, in favor of the AOT engine. */ -@Deprecated(since = "6.1", forRemoval = true) -@SuppressWarnings("removal") public final class CandidateComponentsIndexLoader { /** @@ -119,4 +117,28 @@ private CandidateComponentsIndexLoader() { } } + + /** + * Programmatically add the given index instance for the given ClassLoader, + * replacing a file-determined index with a programmatically composed index. + *

    The index instance will usually be pre-populated for AOT runtime setups + * or test scenarios with pre-configured results for runtime-attempted scans. + * Alternatively, it may be empty for it to get populated during AOT processing + * or a test run, for subsequent introspection the index-recorded candidate types. + * @param classLoader the ClassLoader to add the index for + * @param index the associated CandidateComponentsIndex instance + * @since 7.0 + */ + public static void addIndex(ClassLoader classLoader, CandidateComponentsIndex index) { + cache.put(classLoader, index); + } + + /** + * Clear the runtime index cache. + * @since 7.0 + */ + public static void clearCache() { + cache.clear(); + } + } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 05d8f8ddd3ef..4afe9f1ba9fa 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -66,6 +66,7 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.context.event.ApplicationEventMulticaster; import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextPausedEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.ContextRestartedEvent; import org.springframework.context.event.ContextStartedEvent; @@ -75,6 +76,7 @@ import org.springframework.context.weaving.LoadTimeWeaverAware; import org.springframework.context.weaving.LoadTimeWeaverAwareProcessor; import org.springframework.core.NativeDetector; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; @@ -927,6 +929,9 @@ protected void registerListeners() { */ @SuppressWarnings("unchecked") protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Mark current thread for singleton instantiation with applied bootstrap locking. + beanFactory.prepareSingletonBootstrap(); + // Initialize bootstrap executor for this context. if (beanFactory.containsBean(BOOTSTRAP_EXECUTOR_BEAN_NAME) && beanFactory.isTypeMatch(BOOTSTRAP_EXECUTOR_BEAN_NAME, Executor.class)) { @@ -1299,6 +1304,12 @@ public ObjectProvider getBeanProvider(ResolvableType requiredType) { return getBeanFactory().getBeanProvider(requiredType); } + @Override + public ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType) { + assertBeanFactoryActive(); + return getBeanFactory().getBeanProvider(requiredType); + } + @Override public boolean containsBean(String name) { return getBeanFactory().containsBean(name); @@ -1486,17 +1497,17 @@ public boolean containsLocalBean(String name) { //--------------------------------------------------------------------- @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); } @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); } @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); } @@ -1555,6 +1566,12 @@ public void restart() { publishEvent(new ContextRestartedEvent(this)); } + @Override + public void pause() { + getLifecycleProcessor().onPause(); + publishEvent(new ContextPausedEvent(this)); + } + @Override public boolean isRunning() { return (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()); diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java index 20a2a98d745b..99bb6c6c9f3d 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java @@ -134,7 +134,7 @@ protected boolean isUseCodeAsDefaultMessage() { @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); if (msg != null) { return msg; @@ -146,7 +146,7 @@ protected boolean isUseCodeAsDefaultMessage() { } @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); if (msg != null) { return msg; @@ -155,11 +155,16 @@ public final String getMessage(String code, Object @Nullable [] args, Locale loc if (fallback != null) { return fallback; } - throw new NoSuchMessageException(code, locale); + if (locale == null ) { + throw new NoSuchMessageException(code); + } + else { + throw new NoSuchMessageException(code, locale); + } } @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(); if (codes != null) { for (String code : codes) { @@ -173,7 +178,13 @@ public final String getMessage(MessageSourceResolvable resolvable, Locale locale if (defaultMessage != null) { 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 final String getMessage(MessageSourceResolvable resolvable, Locale locale * @see #renderDefaultMessage(String, Object[], Locale) * @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[] codes = resolvable.getCodes(); if (defaultMessage != null) { @@ -323,7 +334,7 @@ public final String getMessage(MessageSourceResolvable resolvable, Locale locale * @return an array of arguments with any MessageSourceResolvables resolved */ @Override - protected Object[] resolveArguments(Object @Nullable [] args, Locale locale) { + protected Object[] resolveArguments(Object @Nullable [] args, @Nullable Locale locale) { if (ObjectUtils.isEmpty(args)) { return super.resolveArguments(args, locale); } diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java index f5760107f59e..296d624cc179 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java @@ -287,7 +287,7 @@ public void start() { */ @Override public void stop() { - stopBeans(); + stopBeans(false); this.running = false; } @@ -308,7 +308,7 @@ public void onRefresh() { catch (ApplicationContextException ex) { // Some bean failed to auto-start within context refresh: // stop already started beans on context refresh failure. - stopBeans(); + stopBeans(false); throw ex; } this.running = true; @@ -318,15 +318,23 @@ public void onRefresh() { public void onRestart() { this.stoppedBeans = null; if (this.running) { - stopBeans(); + stopBeans(true); } startBeans(true); this.running = true; } + @Override + public void onPause() { + if (this.running) { + stopBeans(true); + this.running = false; + } + } + @Override public void onClose() { - stopBeans(); + stopBeans(false); this.running = false; } @@ -341,7 +349,7 @@ public boolean isRunning() { void stopForRestart() { if (this.running) { this.stoppedBeans = ConcurrentHashMap.newKeySet(); - stopBeans(); + stopBeans(false); this.running = false; } } @@ -361,8 +369,9 @@ private void startBeans(boolean autoStartupOnly) { lifecycleBeans.forEach((beanName, bean) -> { if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) { int startupPhase = getPhase(bean); - phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly)) - .add(beanName, bean); + phases.computeIfAbsent( + startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly, false)) + .add(beanName, bean); } }); @@ -424,14 +433,15 @@ private boolean toBeStarted(String beanName, Lifecycle bean) { (!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup())); } - private void stopBeans() { + private void stopBeans(boolean pauseableOnly) { Map lifecycleBeans = getLifecycleBeans(); Map phases = new TreeMap<>(Comparator.reverseOrder()); lifecycleBeans.forEach((beanName, bean) -> { int shutdownPhase = getPhase(bean); - phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false)) - .add(beanName, bean); + phases.computeIfAbsent( + shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false, pauseableOnly)) + .add(beanName, bean); }); if (!phases.isEmpty()) { @@ -446,13 +456,13 @@ private void stopBeans() { * @param beanName the name of the bean to stop */ private void doStop(Map lifecycleBeans, final String beanName, - final CountDownLatch latch, final Set countDownBeanNames) { + boolean pauseableOnly, final CountDownLatch latch, final Set countDownBeanNames) { Lifecycle bean = lifecycleBeans.remove(beanName); if (bean != null) { String[] dependentBeans = getBeanFactory().getDependentBeans(beanName); for (String dependentBean : dependentBeans) { - doStop(lifecycleBeans, dependentBean, latch, countDownBeanNames); + doStop(lifecycleBeans, dependentBean, pauseableOnly, latch, countDownBeanNames); } try { if (bean.isRunning()) { @@ -461,20 +471,26 @@ private void doStop(Map lifecycleBeans, final Strin stoppedBeans.add(beanName); } if (bean instanceof SmartLifecycle smartLifecycle) { - if (logger.isTraceEnabled()) { - logger.trace("Asking bean '" + beanName + "' of type [" + - bean.getClass().getName() + "] to stop"); + if (!pauseableOnly || smartLifecycle.isPauseable()) { + if (logger.isTraceEnabled()) { + 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(() -> { + else { + // Don't wait for beans that aren't pauseable... latch.countDown(); - countDownBeanNames.remove(beanName); - if (logger.isDebugEnabled()) { - logger.debug("Bean '" + beanName + "' completed its stop procedure"); - } - }); + } } - else { + else if (!pauseableOnly) { if (logger.isTraceEnabled()) { logger.trace("Stopping bean '" + beanName + "' of type [" + bean.getClass().getName() + "]"); @@ -562,14 +578,19 @@ private class LifecycleGroup { private final boolean autoStartupOnly; + private final boolean pauseableOnly; + private final List members = new ArrayList<>(); private int smartMemberCount; - public LifecycleGroup(int phase, Map lifecycleBeans, boolean autoStartupOnly) { + public LifecycleGroup(int phase, Map lifecycleBeans, + boolean autoStartupOnly, boolean pauseableOnly) { + this.phase = phase; this.lifecycleBeans = lifecycleBeans; this.autoStartupOnly = autoStartupOnly; + this.pauseableOnly = pauseableOnly; } public void add(String name, Lifecycle bean) { @@ -621,7 +642,7 @@ public void stop() { Set lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet()); for (LifecycleGroupMember member : this.members) { 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) { // Already removed: must have been a dependent bean from another phase @@ -693,34 +714,35 @@ public void checkpointRestore() { */ private class CracResourceAdapter implements org.crac.Resource { - private @Nullable CyclicBarrier barrier; + private final CyclicBarrier beforeCheckpointBarrier = new CyclicBarrier(2); + private final CyclicBarrier afterRestoreBarrier = new CyclicBarrier(2); @Override public void beforeCheckpoint(org.crac.Context context) { - // A non-daemon thread for preventing an accidental JVM shutdown before the checkpoint - this.barrier = new CyclicBarrier(2); - - Thread thread = new Thread(() -> { - awaitPreventShutdownBarrier(); - // Checkpoint happens here - awaitPreventShutdownBarrier(); - }, "prevent-shutdown"); - + Thread thread = new Thread(this::preventShutdown, "prevent-shutdown"); thread.setDaemon(false); thread.start(); - awaitPreventShutdownBarrier(); logger.debug("Stopping Spring-managed lifecycle beans before JVM checkpoint"); stopForRestart(); } + private void preventShutdown() { + awaitBarrier(this.beforeCheckpointBarrier); + // Checkpoint happens here + awaitBarrier(this.afterRestoreBarrier); + } + @Override public void afterRestore(org.crac.Context context) { + // Unlock barrier for beforeCheckpoint + awaitBarrier(this.beforeCheckpointBarrier); + logger.info("Restarting Spring-managed lifecycle beans after JVM restore"); restartAfterStop(); - // Barrier for prevent-shutdown thread not needed anymore - this.barrier = null; + // Unlock barrier for afterRestore to shutdown "prevent-shutdown" thread + awaitBarrier(this.afterRestoreBarrier); if (!checkpointOnRefresh) { logger.info("Spring-managed lifecycle restart completed (restored JVM running for " + @@ -728,14 +750,12 @@ public void afterRestore(org.crac.Context context) } } - private void awaitPreventShutdownBarrier() { + private void awaitBarrier(CyclicBarrier barrier) { try { - if (this.barrier != null) { - this.barrier.await(); - } + barrier.await(); } catch (Exception ex) { - logger.trace("Exception from prevent-shutdown barrier", ex); + logger.trace("Exception from barrier", ex); } } } diff --git a/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java index 26bc6a0d2752..19ae89b264a3 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java @@ -53,7 +53,7 @@ public void setParentMessageSource(@Nullable MessageSource parent) { @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) { return this.parentMessageSource.getMessage(code, args, defaultMessage, locale); } @@ -66,17 +66,22 @@ else if (defaultMessage != null) { } @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) { return this.parentMessageSource.getMessage(code, args, locale); } else { - throw new NoSuchMessageException(code, locale); + if (locale == null) { + throw new NoSuchMessageException(code); + } + else { + throw new NoSuchMessageException(code, locale); + } } } @Override - public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { + public String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException { if (this.parentMessageSource != null) { return this.parentMessageSource.getMessage(resolvable, locale); } @@ -86,7 +91,12 @@ public String getMessage(MessageSourceResolvable resolvable, Locale locale) thro } String[] codes = resolvable.getCodes(); 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); + } } } diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java index 42b7ce3525c8..93ca113e0772 100644 --- a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java @@ -98,7 +98,7 @@ protected boolean isAlwaysUseMessageFormat() { * @return the rendered default message (with resolved arguments) * @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); } @@ -112,7 +112,7 @@ protected String renderDefaultMessage(String defaultMessage, Object @Nullable [] * @param locale the Locale used for formatting * @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)) { return msg; } @@ -146,7 +146,7 @@ protected String formatMessage(String msg, Object @Nullable [] args, Locale loca * @param locale the Locale to create a {@code MessageFormat} for * @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); } @@ -158,7 +158,7 @@ protected MessageFormat createMessageFormat(String msg, Locale locale) { * @param locale the Locale to resolve against * @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]); } diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index fbc1b285a5ff..03d4d2967897 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -151,9 +151,8 @@ public void setFileEncodings(Properties fileEncodings) { * locked in a refresh attempt for a specific cached properties file whereas * other threads keep returning the old properties for the time being, until * the refresh attempt has completed. - *

    Default is "true": this behavior is new as of Spring Framework 4.1, - * minimizing contention between threads. If you prefer the old behavior, - * i.e. to fully block on refresh, switch this flag to "false". + *

    Default is "true", minimizing contention between threads. If you prefer + * the old behavior, i.e. to fully block on refresh, switch this flag to "false". * @since 4.1 * @see #setCacheSeconds */ diff --git a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java index 8fe88b82229f..78e0d7338768 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/NumberFormat.java @@ -25,18 +25,18 @@ /** * Declares that a field or method parameter should be formatted as a number. * - *

    Supports formatting by style or custom pattern string. Can be applied - * to any JDK {@code Number} type such as {@code Double} and {@code Long}. + *

    Supports formatting by style or custom pattern string. Can be applied to + * any JDK {@code Number} types such as {@code Double} and {@code Long}. * - *

    For style-based formatting, set the {@link #style} attribute to be the - * desired {@link Style}. For custom formatting, set the {@link #pattern} - * attribute to be the number pattern, such as {@code #, ###.##}. + *

    For style-based formatting, set the {@link #style} attribute to the desired + * {@link Style}. For custom formatting, set the {@link #pattern} attribute to the + * desired number pattern, such as {@code "#,###.##"}. * *

    Each attribute is mutually exclusive, so only set one attribute per - * annotation instance (the one most convenient one for your formatting needs). - * When the {@link #pattern} attribute is specified, it takes precedence over - * the {@link #style} attribute. When no annotation attributes are specified, - * the default format applied is style-based for either number of currency, + * annotation (the one most convenient for your formatting needs). When the + * {@link #pattern} attribute is specified, it takes precedence over the + * {@link #style} attribute. When no annotation attributes are specified, the + * default format applied is style-based for either number or currency, * depending on the annotated field or method parameter type. * * @author Keith Donald @@ -50,19 +50,21 @@ public @interface NumberFormat { /** - * The style pattern to use to format the field. + * The style pattern to use to format the field or method parameter. *

    Defaults to {@link Style#DEFAULT} for general-purpose number formatting * for most annotated types, except for money types which default to currency - * formatting. Set this attribute when you wish to format your field in - * accordance with a common style other than the default style. + * formatting. + *

    Set this attribute when you wish to format your field or method parameter + * in accordance with a common style other than the default style. */ Style style() default Style.DEFAULT; /** - * The custom pattern to use to format the field. - *

    Defaults to empty String, indicating no custom pattern String has been specified. - * Set this attribute when you wish to format your field in accordance with a - * custom number pattern not represented by a style. + * The custom pattern to use to format the field or method parameter. + *

    Defaults to an empty String, indicating no custom pattern has been + * specified. + *

    Set this attribute when you wish to format your field or method parameter + * in accordance with a custom number pattern not represented by a style. */ String pattern() default ""; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java index 805670ecb83a..ba719512227c 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -28,7 +28,7 @@ * following JSR-310's parsing rules for an Instant (that is, not using a * configurable {@link java.time.format.DateTimeFormatter}): accepting the * default {@code ISO_INSTANT} format as well as {@code RFC_1123_DATE_TIME} - * (which is commonly used for HTTP date header values), as of Spring 4.3. + * (which is commonly used for HTTP date header values). * * @author Juergen Hoeller * @author Andrei Nevedomskii diff --git a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java index 7358aa4d5f0c..e7df7c258054 100644 --- a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java +++ b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java @@ -47,11 +47,11 @@ */ public class DefaultFormattingConversionService extends FormattingConversionService { - private static final boolean jsr354Present; + private static final boolean JSR_354_PRESENT; static { ClassLoader classLoader = DefaultFormattingConversionService.class.getClassLoader(); - jsr354Present = ClassUtils.isPresent("javax.money.MonetaryAmount", classLoader); + JSR_354_PRESENT = ClassUtils.isPresent("javax.money.MonetaryAmount", classLoader); } /** @@ -107,7 +107,7 @@ public static void addDefaultFormatters(FormatterRegistry formatterRegistry) { formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); // Default handling of monetary values - if (jsr354Present) { + if (JSR_354_PRESENT) { formatterRegistry.addFormatter(new CurrencyUnitFormatter()); formatterRegistry.addFormatter(new MonetaryAmountFormatter()); formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory()); diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java index 75483393e963..61f43dbedf16 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java @@ -35,7 +35,7 @@ * Thanks to Ales Justin and Marius Bogoevici for the initial prototype. * *

    This weaver supports WildFly 13-23 (DelegatingClassFileTransformer) as well as - * WildFly 24+ (DelegatingClassTransformer), as of Spring Framework 6.1.15. + * WildFly 24+ (DelegatingClassTransformer). * * @author Costin Leau * @author Juergen Hoeller diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java index 6009d6bdcf35..033eb4dbc99d 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java @@ -43,7 +43,7 @@ public class JndiLocatorDelegate extends JndiLocatorSupport { * JNDI lookups such as for a {@code DataSource} or some other environment resource. * The flag literally just affects code which attempts JNDI searches based on the * {@code JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()} check: in particular, - * {@code StandardServletEnvironment} and {@code StandardPortletEnvironment}. + * {@code StandardServletEnvironment}. * @since 4.3 * @see #isDefaultJndiEnvironmentAvailable() * @see JndiPropertySource diff --git a/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java b/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java index 29d85450387d..cd6f5feb498e 100644 --- a/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java +++ b/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java @@ -34,6 +34,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.jndi.JndiLocatorSupport; import org.springframework.jndi.TypeMismatchNamingException; @@ -195,6 +196,12 @@ public ObjectProvider getBeanProvider(ResolvableType requiredType) { "SimpleJndiBeanFactory does not support resolution by ResolvableType"); } + @Override + public ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType) { + throw new UnsupportedOperationException( + "SimpleJndiBeanFactory does not support resolution by ParameterizedTypeReference"); + } + @Override public boolean containsBean(String name) { if (this.singletonObjects.containsKey(name) || this.resourceTypes.containsKey(name)) { diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java b/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java index 465ba44bb5b6..d6a2750cce7a 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java @@ -23,6 +23,7 @@ import java.lang.annotation.Target; import org.springframework.aot.hint.annotation.Reflective; +import org.springframework.core.annotation.AliasFor; /** * A common annotation specifying a concurrency limit for an individual method, @@ -37,15 +38,21 @@ * *

    This is particularly useful with Virtual Threads where there is generally * no thread pool limit in place. For asynchronous tasks, this can be constrained - * on {@link org.springframework.core.task.SimpleAsyncTaskExecutor}; for + * on {@link org.springframework.core.task.SimpleAsyncTaskExecutor}. For * synchronous invocations, this annotation provides equivalent behavior through * {@link org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor}. + * Alternatively, consider {@link org.springframework.core.task.SyncTaskExecutor} + * and its inherited concurrency throttling support (new as of 7.0) for + * programmatic use. * * @author Juergen Hoeller + * @author Hyunsang Han + * @author Sam Brannen * @since 7.0 * @see EnableResilientMethods * @see ConcurrencyLimitBeanPostProcessor * @see org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor + * @see org.springframework.core.task.SyncTaskExecutor#setConcurrencyLimit * @see org.springframework.core.task.SimpleAsyncTaskExecutor#setConcurrencyLimit */ @Target({ElementType.TYPE, ElementType.METHOD}) @@ -55,11 +62,37 @@ public @interface ConcurrencyLimit { /** - * The applicable concurrency limit: 1 by default, - * effectively locking the target instance for each method invocation. - *

    Specify a limit higher than 1 for pool-like throttling, constraining + * Alias for {@link #limit()}. + *

    Intended to be used when no other attributes are needed — for + * example, {@code @ConcurrencyLimit(5)}. + * @see #limitString() + */ + @AliasFor("limit") + int value() default Integer.MIN_VALUE; + + /** + * The concurrency limit. + *

    Specify {@code 1} to effectively lock the target instance for each method + * invocation. + *

    Specify a limit greater than {@code 1} for pool-like throttling, constraining * the number of concurrent invocations similar to the upper bound of a pool. + *

    Specify {@code -1} for unbounded concurrency. + * @see #value() + * @see #limitString() + * @see org.springframework.util.ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY + */ + @AliasFor("value") + int limit() default Integer.MIN_VALUE; + + /** + * The concurrency limit, as a configurable String. + *

    A non-empty value specified here overrides the {@link #limit()} and + * {@link #value()} attributes. + *

    This supports Spring-style "${...}" placeholders as well as SpEL expressions. + *

    See the Javadoc for {@link #limit()} for details on supported values. + * @see #limit() + * @see org.springframework.util.ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY */ - int value() default 1; + String limitString() default ""; } diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java index c556c7d52bae..99cf5fcdc1bd 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -31,9 +32,12 @@ import org.springframework.aop.support.ComposablePointcut; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; /** * A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor @@ -41,10 +45,15 @@ * annotated with {@link ConcurrencyLimit @ConcurrencyLimit}. * * @author Juergen Hoeller + * @author Hyunsang Han * @since 7.0 */ @SuppressWarnings("serial") -public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { +public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor + implements EmbeddedValueResolverAware { + + private @Nullable StringValueResolver embeddedValueResolver; + public ConcurrencyLimitBeanPostProcessor() { setBeforeExistingAdvisors(true); @@ -57,9 +66,15 @@ public ConcurrencyLimitBeanPostProcessor() { } - private static class ConcurrencyLimitInterceptor implements MethodInterceptor { + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + + private class ConcurrencyLimitInterceptor implements MethodInterceptor { - private final Map cachePerInstance = + private final ConcurrentMap cachePerInstance = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); @Override @@ -68,32 +83,39 @@ private static class ConcurrencyLimitInterceptor implements MethodInterceptor { Object target = invocation.getThis(); Class targetClass = (target != null ? target.getClass() : method.getDeclaringClass()); if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) { - // Allow validation for AOP proxy without a target + // Support concurrency throttling for AOP proxy without a target target = methodInvocation.getProxy(); } Assert.state(target != null, "Target must not be null"); + // Build unique ConcurrencyThrottleCache instance per target object ConcurrencyThrottleCache cache = this.cachePerInstance.computeIfAbsent(target, k -> new ConcurrencyThrottleCache()); + + // Determine method-specific interceptor instance with isolated concurrency count MethodInterceptor interceptor = cache.methodInterceptors.get(method); if (interceptor == null) { synchronized (cache) { interceptor = cache.methodInterceptors.get(method); if (interceptor == null) { boolean perMethod = false; - ConcurrencyLimit limit = AnnotatedElementUtils.getMergedAnnotation(method, ConcurrencyLimit.class); - if (limit != null) { + ConcurrencyLimit annotation = AnnotatedElementUtils.getMergedAnnotation(method, ConcurrencyLimit.class); + if (annotation != null) { perMethod = true; } else { interceptor = cache.classInterceptor; if (interceptor == null) { - limit = AnnotatedElementUtils.getMergedAnnotation(targetClass, ConcurrencyLimit.class); + annotation = AnnotatedElementUtils.getMergedAnnotation(targetClass, ConcurrencyLimit.class); } } if (interceptor == null) { - Assert.state(limit != null, "No @ConcurrencyLimit annotation found"); - interceptor = new ConcurrencyThrottleInterceptor(limit.value()); + Assert.state(annotation != null, "No @ConcurrencyLimit annotation found"); + int concurrencyLimit = parseInt(annotation.limit(), annotation.limitString()); + if (concurrencyLimit < -1) { + throw new IllegalStateException(annotation + " must be configured with a valid limit"); + } + interceptor = new ConcurrencyThrottleInterceptor(concurrencyLimit); if (!perMethod) { cache.classInterceptor = interceptor; } @@ -104,6 +126,18 @@ private static class ConcurrencyLimitInterceptor implements MethodInterceptor { } return interceptor.invoke(invocation); } + + private int parseInt(int value, String stringValue) { + if (StringUtils.hasText(stringValue)) { + if (embeddedValueResolver != null) { + stringValue = embeddedValueResolver.resolveStringValue(stringValue); + } + if (StringUtils.hasText(stringValue)) { + return Integer.parseInt(stringValue); + } + } + return value; + } } diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/EnableResilientMethods.java b/spring-context/src/main/java/org/springframework/resilience/annotation/EnableResilientMethods.java index 4facad6d4e3a..e8c00bfc8879 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/EnableResilientMethods.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/EnableResilientMethods.java @@ -60,10 +60,10 @@ /** * Indicate the order in which the {@link RetryAnnotationBeanPostProcessor} * and {@link ConcurrencyLimitBeanPostProcessor} should be applied. - *

    The default is {@link Ordered#LOWEST_PRECEDENCE} in order to run - * after all other post-processors, so that they can add advisors to - * existing proxies rather than double-proxy. + *

    The default is {@link Ordered#LOWEST_PRECEDENCE - 1} in order to run + * after all common post-processors, except for {@code @EnableAsync}. + * @see org.springframework.scheduling.annotation.EnableAsync#order() */ - int order() default Ordered.LOWEST_PRECEDENCE; + int order() default Ordered.LOWEST_PRECEDENCE - 1; } diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java index e9d80657d41f..3b402856e807 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java @@ -98,7 +98,7 @@ private class RetryAnnotationInterceptor extends AbstractRetryInterceptor { retrySpec = new MethodRetrySpec( Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()), instantiatePredicate(retryable.predicate()), - parseLong(retryable.maxAttempts(), retryable.maxAttemptsString()), + parseLong(retryable.maxRetries(), retryable.maxRetriesString()), parseDuration(retryable.delay(), retryable.delayString(), timeUnit), parseDuration(retryable.jitter(), retryable.jitterString(), timeUnit), parseDouble(retryable.multiplier(), retryable.multiplierString()), diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java b/spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java index 3f27d2083d2b..b9851b258528 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/Retryable.java @@ -40,6 +40,7 @@ * project but redesigned as a minimal core retry feature in the Spring Framework. * * @author Juergen Hoeller + * @author Sam Brannen * @since 7.0 * @see EnableResilientMethods * @see RetryAnnotationBeanPostProcessor @@ -64,6 +65,11 @@ /** * Applicable exception types to attempt a retry for. This attribute * allows for the convenient specification of assignable exception types. + *

    The supplied exception types will be matched against an exception + * thrown by a failed invocation as well as nested + * {@linkplain Throwable#getCause() causes}. + *

    This can optionally be combined with {@link #excludes() excludes} or + * a custom {@link #predicate() predicate}. *

    The default is empty, leading to a retry attempt for any exception. * @see #excludes() * @see #predicate() @@ -74,6 +80,11 @@ /** * Non-applicable exception types to avoid a retry for. This attribute * allows for the convenient specification of assignable exception types. + *

    The supplied exception types will be matched against an exception + * thrown by a failed invocation as well as nested + * {@linkplain Throwable#getCause() causes}. + *

    This can optionally be combined with {@link #includes() includes} or + * a custom {@link #predicate() predicate}. *

    The default is empty, leading to a retry attempt for any exception. * @see #includes() * @see #predicate() @@ -81,30 +92,35 @@ Class[] excludes() default {}; /** - * A predicate for filtering applicable exceptions for which - * an invocation can be retried. - *

    The default is a retry attempt for any exception. + * A predicate for filtering applicable exceptions for which an invocation can + * be retried. *

    A specified {@link MethodRetryPredicate} implementation will be instantiated * per method. It can use dependency injection at the constructor level or through * autowiring annotations, in case it needs access to other beans or facilities. + *

    This can optionally be combined with {@link #includes() includes} or + * {@link #excludes() excludes}. + *

    The default is a retry attempt for any exception. * @see #includes() * @see #excludes() */ Class predicate() default MethodRetryPredicate.class; /** - * The maximum number of retry attempts, in addition to the initial invocation. + * The maximum number of retry attempts. + *

    Note that {@code total attempts = 1 initial attempt + maxRetries attempts}. + * Thus, if {@code maxRetries} is set to 4, the annotated method will be invoked + * at least once and at most 5 times. *

    The default is 3. */ - long maxAttempts() default 3; + long maxRetries() default 3; /** * The maximum number of retry attempts, as a configurable String. - * A non-empty value specified here overrides the {@link #maxAttempts()} attribute. + *

    A non-empty value specified here overrides the {@link #maxRetries()} attribute. *

    This supports Spring-style "${...}" placeholders as well as SpEL expressions. - * @see #maxAttempts() + * @see #maxRetries() */ - String maxAttemptsString() default ""; + String maxRetriesString() default ""; /** * The base delay after the initial invocation. If a multiplier is specified, @@ -120,7 +136,7 @@ /** * The base delay after the initial invocation, as a duration String. - * A non-empty value specified here overrides the {@link #delay()} attribute. + *

    A non-empty value specified here overrides the {@link #delay()} attribute. *

    The duration String can be in several formats: *