Merge branch 'main' into 35179
This commit is contained in:
commit
832689925c
|
@ -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' || '' }}
|
||||
24
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
|
|
|
@ -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=24.0.2-librca
|
||||
|
|
|
@ -2,11 +2,11 @@ plugins {
|
|||
id 'io.freefair.aspectj' version '8.13.1' apply false
|
||||
// kotlinVersion is managed in gradle.properties
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka' version '1.9.20'
|
||||
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 'me.champeau.jmh' version '0.7.2' apply false
|
||||
id "io.spring.nullability" version "0.0.1" apply false
|
||||
id 'io.spring.nullability' version '0.0.4' apply false
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -75,7 +75,7 @@ configure([rootProject] + javaProjects) { project ->
|
|||
"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/5.13.4/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/",
|
||||
|
|
|
@ -20,6 +20,7 @@ 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.gradle:test-retry-gradle-plugin:1.6.2"
|
||||
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
|
||||
|
|
|
@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
|||
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("11.0.0");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
package org.springframework.build;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.SourceSetContainer;
|
||||
import org.jetbrains.dokka.gradle.DokkaExtension;
|
||||
import org.jetbrains.dokka.gradle.DokkaPlugin;
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget;
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion;
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
|
||||
|
@ -28,8 +32,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
|
|||
public class KotlinConventions {
|
||||
|
||||
void apply(Project project) {
|
||||
project.getPlugins().withId("org.jetbrains.kotlin.jvm",
|
||||
(plugin) -> project.getTasks().withType(KotlinCompile.class, this::configure));
|
||||
project.getPlugins().withId("org.jetbrains.kotlin.jvm", plugin -> {
|
||||
project.getTasks().withType(KotlinCompile.class, this::configure);
|
||||
if (project.getLayout().getProjectDirectory().dir("src/main/kotlin").getAsFile().exists()) {
|
||||
project.getPlugins().apply(DokkaPlugin.class);
|
||||
project.getExtensions().configure(DokkaExtension.class, dokka -> configure(project, dokka));
|
||||
project.project(":framework-api").getDependencies().add("dokka", project);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configure(KotlinCompile compile) {
|
||||
|
@ -49,4 +59,35 @@ public class KotlinConventions {
|
|||
});
|
||||
}
|
||||
|
||||
private void configure(Project project, DokkaExtension dokka) {
|
||||
dokka.getDokkaSourceSets().forEach(sourceSet -> {
|
||||
sourceSet.getSourceRoots().setFrom(project.file("src/main/kotlin"));
|
||||
sourceSet.getClasspath()
|
||||
.from(project.getExtensions()
|
||||
.getByType(SourceSetContainer.class)
|
||||
.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
|
||||
.getOutput());
|
||||
var externalDocumentationLinks = sourceSet.getExternalDocumentationLinks();
|
||||
var springVersion = project.getVersion();
|
||||
externalDocumentationLinks.register("spring-framework", spec -> {
|
||||
spec.url("https://docs.spring.io/spring-framework/docs/" + springVersion + "/javadoc-api/");
|
||||
spec.packageListUrl("https://docs.spring.io/spring-framework/docs/" + springVersion + "/javadoc-api/element-list");
|
||||
});
|
||||
externalDocumentationLinks.register("reactor-core", spec ->
|
||||
spec.url("https://projectreactor.io/docs/core/release/api/"));
|
||||
externalDocumentationLinks.register("reactive-streams", spec ->
|
||||
spec.url("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/"));
|
||||
externalDocumentationLinks.register("kotlinx-coroutines", spec ->
|
||||
spec.url("https://kotlinlang.org/api/kotlinx.coroutines/"));
|
||||
externalDocumentationLinks.register("hamcrest", spec ->
|
||||
spec.url("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/"));
|
||||
externalDocumentationLinks.register("jakarta-servlet", spec -> {
|
||||
spec.url("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/");
|
||||
spec.packageListUrl("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list");
|
||||
});
|
||||
externalDocumentationLinks.register("rsocket-core", spec ->
|
||||
spec.url("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/"));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.Dependency;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
import org.gradle.api.tasks.testing.TestFrameworkOptions;
|
||||
|
@ -26,12 +26,16 @@ import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions;
|
|||
import org.gradle.testretry.TestRetryPlugin;
|
||||
import org.gradle.testretry.TestRetryTaskExtension;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Conventions that are applied in the presence of the {@link JavaBasePlugin}. When the
|
||||
* plugin is applied:
|
||||
* <ul>
|
||||
* <li>The {@link TestRetryPlugin Test Retry} plugin is applied so that flaky tests
|
||||
* are retried 3 times when running on the CI server.
|
||||
* <li>Common test properties are configured
|
||||
* <li>The ByteBuddy Java agent is configured on test tasks.
|
||||
* </ul>
|
||||
*
|
||||
* @author Brian Clozel
|
||||
|
@ -45,6 +49,7 @@ class TestConventions {
|
|||
}
|
||||
|
||||
private void configureTestConventions(Project project) {
|
||||
configureByteBuddyAgent(project);
|
||||
project.getTasks().withType(Test.class,
|
||||
test -> {
|
||||
configureTests(project, test);
|
||||
|
@ -75,6 +80,20 @@ class TestConventions {
|
|||
);
|
||||
}
|
||||
|
||||
private void configureByteBuddyAgent(Project project) {
|
||||
if (project.hasProperty("byteBuddyVersion")) {
|
||||
String byteBuddyVersion = (String) project.getProperties().get("byteBuddyVersion");
|
||||
Configuration byteBuddyAgentConfig = project.getConfigurations().create("byteBuddyAgentConfig");
|
||||
byteBuddyAgentConfig.setTransitive(false);
|
||||
Dependency byteBuddyAgent = project.getDependencies().create("net.bytebuddy:byte-buddy-agent:" + byteBuddyVersion);
|
||||
byteBuddyAgentConfig.getDependencies().add(byteBuddyAgent);
|
||||
project.afterEvaluate(p -> {
|
||||
p.getTasks().withType(Test.class, test -> test
|
||||
.jvmArgs("-javaagent:" + byteBuddyAgentConfig.getAsPath()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void configureTestRetryPlugin(Project project, Test test) {
|
||||
project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> {
|
||||
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
plugins {
|
||||
id 'java-platform'
|
||||
id 'io.freefair.aggregate-javadoc' version '8.13.1'
|
||||
id 'org.jetbrains.dokka'
|
||||
}
|
||||
|
||||
description = "Spring Framework API Docs"
|
||||
|
@ -54,23 +55,19 @@ javadoc {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce KDoc for all Spring Framework modules in "build/docs/kdoc"
|
||||
*/
|
||||
rootProject.tasks.dokkaHtmlMultiModule.configure {
|
||||
dependsOn {
|
||||
tasks.named("javadoc")
|
||||
dokka {
|
||||
moduleName = "spring-framework"
|
||||
dokkaPublications.html {
|
||||
outputDirectory = project.java.docsDir.dir("kdoc-api")
|
||||
includes.from("$rootProject.rootDir/framework-docs/src/docs/api/dokka-overview.md")
|
||||
}
|
||||
moduleName.set("spring-framework")
|
||||
outputDirectory.set(project.java.docsDir.dir("kdoc-api").get().asFile)
|
||||
includes.from("$rootProject.rootDir/framework-docs/src/docs/api/dokka-overview.md")
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip all Java docs (javadoc & kdoc) into a single archive
|
||||
*/
|
||||
tasks.register('docsZip', Zip) {
|
||||
dependsOn = ['javadoc', rootProject.tasks.dokkaHtmlMultiModule]
|
||||
dependsOn = ['javadoc', 'dokkaGenerate']
|
||||
group = "distribution"
|
||||
description = "Builds -${archiveClassifier} archive containing api and reference " +
|
||||
"for deployment at https://docs.spring.io/spring-framework/docs/."
|
||||
|
@ -83,7 +80,7 @@ tasks.register('docsZip', Zip) {
|
|||
from(javadoc) {
|
||||
into "javadoc-api"
|
||||
}
|
||||
from(rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) {
|
||||
from(project.java.docsDir.dir("kdoc-api")) {
|
||||
into "kdoc-api"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -197,6 +197,7 @@
|
|||
*** xref:web/webmvc-functional.adoc[]
|
||||
*** xref:web/webmvc/mvc-uri-building.adoc[]
|
||||
*** xref:web/webmvc/mvc-ann-async.adoc[]
|
||||
*** xref:web/webmvc/mvc-range.adoc[]
|
||||
*** xref:web/webmvc-cors.adoc[]
|
||||
*** xref:web/webmvc-versioning.adoc[]
|
||||
*** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[]
|
||||
|
@ -294,6 +295,7 @@
|
|||
**** xref:web/webflux/controller/ann-advice.adoc[]
|
||||
*** xref:web/webflux-functional.adoc[]
|
||||
*** xref:web/webflux/uri-building.adoc[]
|
||||
*** xref:web/webflux/range.adoc[]
|
||||
*** xref:web/webflux-cors.adoc[]
|
||||
*** xref:web/webflux-versioning.adoc[]
|
||||
*** xref:web/webflux/ann-rest-exceptions.adoc[]
|
||||
|
@ -353,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[]
|
||||
|
|
|
@ -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 `<aop:config>` element to true, as follows:
|
||||
|
||||
|
@ -60,6 +64,24 @@ To be clear, using `proxy-target-class="true"` on `<tx:annotation-driven/>`,
|
|||
proxies _for all three of them_.
|
||||
====
|
||||
|
||||
`@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
|
||||
|
|
|
@ -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 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
|
||||
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.
|
||||
====
|
||||
|
||||
|
||||
|
@ -828,10 +830,10 @@ definitions, there is no notion of bean definition inheritance, and inheritance
|
|||
hierarchies at the class level are irrelevant for metadata purposes.
|
||||
|
||||
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 +875,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:
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
====
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<String, String> map = new HashMap<>();
|
||||
map.put("Name", "Mark");
|
||||
map.put("Age", new Integer(47));
|
||||
jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() {
|
||||
public Message postProcessMessage(Message message) throws JMSException {
|
||||
message.setIntProperty("AccountID", 1234);
|
||||
message.setJMSCorrelationID("123-00001");
|
||||
return message;
|
||||
}
|
||||
});
|
||||
}
|
||||
----
|
||||
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[]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1195,6 +1195,46 @@ One way to declare HTTP Service groups is via `@ImportHttpServices` annotations
|
|||
<1> Manually list interfaces for group "echo"
|
||||
<2> Detect interfaces for group "greeting" under a base package
|
||||
|
||||
The above lets you declare HTTP Services and groups. As an alternative, you can also
|
||||
annotate HTTP interfaces as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@HttpServiceClient("echo")
|
||||
public interface EchoServiceA {
|
||||
// ...
|
||||
}
|
||||
|
||||
@HttpServiceClient("echo")
|
||||
public interface EchoServiceB {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
The above requires a dedicated import registrar as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public class MyClientHttpServiceRegistrar implements AbstractClientHttpServiceRegistrar { // <1>
|
||||
|
||||
@Override
|
||||
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
|
||||
findAndRegisterHttpServiceClients(groupRegistry, List.of("org.example.echo")); // <2>
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(MyClientHttpServiceRegistrar.class) // <3>
|
||||
public class ClientConfig {
|
||||
}
|
||||
----
|
||||
<1> Extend dedicated `AbstractClientHttpServiceRegistrar`
|
||||
<2> Specify base packages where to find client interfaces
|
||||
<3> Import the registrar
|
||||
|
||||
TIP: `@HttpServiceClient` interfaces are excluded from `@ImportHttpServices` scans, so there
|
||||
is no overlap with scans for client interfaces when pointed at the same package.
|
||||
|
||||
It is also possible to declare groups programmatically by creating an HTTP Service
|
||||
registrar and then importing it:
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ As of Spring Framework 6.0, Spring has been upgraded to the Jakarta EE 9 level
|
|||
traditional `javax` packages. With EE 9 as the minimum and EE 10 supported already,
|
||||
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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -0,0 +1,434 @@
|
|||
[[resttestclient]]
|
||||
= RestTestClient
|
||||
|
||||
`RestTestClient` is an HTTP client designed for testing server applications. It wraps
|
||||
Spring's xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and uses it to perform requests,
|
||||
but exposes a testing facade for verifying responses. `RestTestClient` can be used to
|
||||
perform end-to-end HTTP tests. It can also be used to test Spring MVC
|
||||
applications without a running server via MockMvc.
|
||||
|
||||
|
||||
|
||||
|
||||
[[resttestclient-setup]]
|
||||
== Setup
|
||||
|
||||
To set up a `RestTestClient` you need to choose a server setup to bind to. This can be one
|
||||
of several MockMvc setup choices, or a connection to a live server.
|
||||
|
||||
|
||||
|
||||
[[resttestclient-controller-config]]
|
||||
=== Bind to Controller
|
||||
|
||||
This setup allows you to test specific controller(s) via mock request and response objects,
|
||||
without a running server.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestTestClient client =
|
||||
RestTestClient.bindToController(new TestController()).build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val client = RestTestClient.bindToController(TestController()).build()
|
||||
----
|
||||
======
|
||||
|
||||
[[resttestclient-context-config]]
|
||||
=== Bind to `ApplicationContext`
|
||||
|
||||
This setup allows you to load Spring configuration with Spring MVC
|
||||
infrastructure and controller declarations and use it to handle requests via mock request
|
||||
and response objects, without a running server.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(WebConfig.class) // <1>
|
||||
class MyTests {
|
||||
|
||||
RestTestClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp(ApplicationContext context) { // <2>
|
||||
client = RestTestClient.bindToApplicationContext(context).build(); // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Specify the configuration to load
|
||||
<2> Inject the configuration
|
||||
<3> Create the `RestTestClient`
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(WebConfig::class) // <1>
|
||||
class MyTests {
|
||||
|
||||
lateinit var client: RestTestClient
|
||||
|
||||
@BeforeEach
|
||||
fun setUp(context: ApplicationContext) { // <2>
|
||||
client = RestTestClient.bindToApplicationContext(context).build() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Specify the configuration to load
|
||||
<2> Inject the configuration
|
||||
<3> Create the `RestTestClient`
|
||||
======
|
||||
|
||||
[[resttestclient-fn-config]]
|
||||
=== Bind to Router Function
|
||||
|
||||
This setup allows you to test xref:web/webmvc-functional.adoc[functional endpoints] via
|
||||
mock request and response objects, without a running server.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RouterFunction<?> route = ...
|
||||
client = RestTestClient.bindToRouterFunction(route).build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val route: RouterFunction<*> = ...
|
||||
val client = RestTestClient.bindToRouterFunction(route).build()
|
||||
----
|
||||
======
|
||||
|
||||
[[resttestclient-server-config]]
|
||||
=== Bind to Server
|
||||
|
||||
This setup connects to a running server to perform full, end-to-end HTTP tests:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client = RestTestClient.bindToServer().baseUrl("http://localhost:8080").build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
[[resttestclient-client-config]]
|
||||
=== Client Config
|
||||
|
||||
In addition to the server setup options described earlier, you can also configure client
|
||||
options, including base URL, default headers, client filters, and others. These options
|
||||
are readily available following the initial `bindTo` call, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client = RestTestClient.bindToController(new TestController())
|
||||
.baseUrl("/test")
|
||||
.build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client = RestTestClient.bindToController(TestController())
|
||||
.baseUrl("/test")
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
[[resttestclient-tests]]
|
||||
== Writing Tests
|
||||
|
||||
`RestTestClient` provides an API identical to xref:integration/rest-clients.adoc#rest-restclient[`RestClient`]
|
||||
up to the point of performing a request by using `exchange()`.
|
||||
|
||||
After the call to `exchange()`, `RestTestClient` diverges from `RestClient`, and
|
||||
instead continues with a workflow to verify responses.
|
||||
|
||||
To assert the response status and headers, use the following:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
----
|
||||
======
|
||||
|
||||
If you would like for all expectations to be asserted even if one of them fails, you can
|
||||
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is
|
||||
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in
|
||||
JUnit Jupiter.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectAll(
|
||||
spec -> spec.expectStatus().isOk(),
|
||||
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectAll(
|
||||
{ spec -> spec.expectStatus().isOk() },
|
||||
{ spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) }
|
||||
)
|
||||
----
|
||||
======
|
||||
|
||||
You can then choose to decode the response body through one of the following:
|
||||
|
||||
* `expectBody(Class<T>)`: Decode to single object.
|
||||
* `expectBody()`: Decode to `byte[]` for xref:testing/resttestclient.adoc#resttestclient-json[JSON Content] or an empty body.
|
||||
|
||||
|
||||
If the built-in assertions are insufficient, you can consume the object instead and
|
||||
perform any other assertions:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.consumeWith(result -> {
|
||||
// custom assertions (for example, AssertJ)...
|
||||
});
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody<Person>()
|
||||
.consumeWith {
|
||||
// custom assertions (for example, AssertJ)...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Or you can exit the workflow and obtain a `EntityExchangeResult`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectBody<Person>()
|
||||
.returnResult()
|
||||
----
|
||||
======
|
||||
|
||||
TIP: When you need to decode to a target type with generics, look for the overloaded methods
|
||||
that accept {spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`]
|
||||
instead of `Class<T>`.
|
||||
|
||||
|
||||
|
||||
[[resttestclient-no-content]]
|
||||
=== No Content
|
||||
|
||||
If the response is not expected to have content, you can assert that as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.post().uri("/persons")
|
||||
.body(person)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.post().uri("/persons")
|
||||
.body(person)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty()
|
||||
----
|
||||
======
|
||||
|
||||
If you want to ignore the response content, the following releases the content without any assertions:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound()
|
||||
.expectBody(Void.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound
|
||||
.expectBody<Unit>()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
[[resttestclient-json]]
|
||||
=== JSON Content
|
||||
|
||||
You can use `expectBody()` without a target type to perform assertions on the raw
|
||||
content rather than through higher level Object(s).
|
||||
|
||||
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
----
|
||||
======
|
||||
|
||||
To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].name").isEqualTo("Jane")
|
||||
.jsonPath("$[1].name").isEqualTo("Jason");
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].name").isEqualTo("Jane")
|
||||
.jsonPath("$[1].name").isEqualTo("Jason")
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
|
@ -2,10 +2,10 @@
|
|||
= Application Events
|
||||
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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<String>` to extract attributes from the `WebSession` and insert them
|
||||
|
@ -446,7 +446,7 @@ specify CORS settings by URL pattern. If both are specified, they are combined b
|
|||
=== Client
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -93,6 +93,10 @@ You can map requests by using glob patterns and wildcards:
|
|||
|===
|
||||
|Pattern |Description |Example
|
||||
|
||||
| `spring`
|
||||
| Literal pattern
|
||||
| `+"/spring"+` matches `+"/spring"+`
|
||||
|
||||
| `+?+`
|
||||
| Matches one character
|
||||
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
|
||||
|
@ -104,23 +108,41 @@ You can map requests by using glob patterns and wildcards:
|
|||
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`
|
||||
|
||||
| `+**+`
|
||||
| Matches zero or more path segments until the end of the path
|
||||
| Matches zero or more path segments
|
||||
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
|
||||
|
||||
`+"/resources/**/file.png"+` is invalid as `+**+` is only allowed at the end of the path.
|
||||
`+"/**/resources"+` matches `+"/spring/resources"+` and `+"/spring/framework/resources"+`
|
||||
|
||||
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
|
||||
|
||||
`+"/**/{name}/resources"+` is invalid as only a literal pattern is allowed right after `+**+`.
|
||||
`+"/**/project/{project}/resources"+` is allowed.
|
||||
|
||||
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|
||||
|
||||
| `+{name}+`
|
||||
| Matches a path segment and captures it as a variable named "name"
|
||||
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
|
||||
|
||||
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
|
||||
|
||||
| `+{name:[a-z]+}+`
|
||||
| Matches the regexp `+"[a-z]+"+` as a path variable named "name"
|
||||
| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+`
|
||||
|
||||
| `+{*path}+`
|
||||
| Matches zero or more path segments until the end of the path and captures it as a variable named "path"
|
||||
| Matches zero or more path segments and captures it as a variable named "path"
|
||||
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
|
||||
|
||||
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
|
||||
|
||||
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
|
||||
|
||||
`+"/{*path}/{name}/resources"+` is invalid as only a literal pattern is allowed right after `{*path}`.
|
||||
`+"/{*path}/project/{project}/resources"+` is allowed.
|
||||
|
||||
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|
||||
|
||||
|===
|
||||
|
||||
Captured URI variables can be accessed with `@PathVariable`, as the following example shows:
|
||||
|
@ -234,10 +256,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
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
[[webflux-range]]
|
||||
= Range Requests
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-range.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux supports https://datatracker.ietf.org/doc/html/rfc9110#section-14[RFC 9110]
|
||||
range requests. For an overview, see the
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests[Ranger Requests]
|
||||
Mozilla guide.
|
||||
|
||||
The `Range` header is parsed and handled transparently in WebFlux when an annotated
|
||||
controller returns a `Resource` or `ResponseEntity<Resource>`, or a functional endpoint
|
||||
xref:web/webflux-functional.adoc#webflux-fn-resources[serves a `Resource`]. `Range` header
|
||||
support is also transparently handled when serving
|
||||
xref:web/webflux/config.adoc#webflux-config-static-resources[static resources].
|
||||
|
||||
TIP: The `Resource` must not be an `InputStreamResource` and with `ResponseEntity<Resource>`,
|
||||
the status of the response must be 200.
|
||||
|
||||
The underlying support is in the `HttpRange` class, which exposes methods to parse
|
||||
`Range` headers and split a `Resource` into a `List<ResourceRegion>` that in turn can be
|
||||
then written to the response via `ResourceRegionEncoder` and `ResourceHttpMessageWriter`.
|
|
@ -7,7 +7,7 @@ applications:
|
|||
* For server request processing there are two levels of support.
|
||||
** 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]
|
||||
======
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -88,37 +88,71 @@ 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:
|
||||
`@RequestMapping` methods can be mapped using URL patterns.
|
||||
Spring MVC is using `PathPattern` -- a pre-parsed pattern matched against the URL path also pre-parsed as `PathContainer`.
|
||||
Designed for web use, this solution deals effectively with encoding and path parameters, and matches efficiently.
|
||||
See xref:web/webmvc/mvc-config/path-matching.adoc[MVC config] for customizations of path matching options.
|
||||
|
||||
* `PathPattern` -- a pre-parsed pattern matched against the URL path also pre-parsed as
|
||||
`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
|
||||
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.
|
||||
|
||||
`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.
|
||||
You can map requests by using glob patterns and wildcards:
|
||||
|
||||
`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].
|
||||
[cols="2,3,5"]
|
||||
|===
|
||||
|Pattern |Description |Example
|
||||
|
||||
Some example patterns:
|
||||
| `spring`
|
||||
| Literal pattern
|
||||
| `+"/spring"+` matches `+"/spring"+`
|
||||
|
||||
* `+"/resources/ima?e.png"+` - match one character in a path segment
|
||||
* `+"/resources/*.png"+` - match zero or more characters in a path segment
|
||||
* `+"/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
|
||||
| `+?+`
|
||||
| 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
|
||||
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
|
||||
|
||||
`+"/**/resources"+` matches `+"/spring/resources"+` and `+"/spring/framework/resources"+`
|
||||
|
||||
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
|
||||
|
||||
`+"/**/{name}/resources"+` is invalid as only a literal pattern is allowed right after `+**+`.
|
||||
`+"/**/project/{project}/resources"+` is allowed.
|
||||
|
||||
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|
||||
|
||||
| `+{name}+`
|
||||
| Matches a path segment and captures it as a variable named "name"
|
||||
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
|
||||
|
||||
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
|
||||
|
||||
| `+{name:[a-z]+}+`
|
||||
| Matches the regexp `+"[a-z]+"+` as a path variable named "name"
|
||||
| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+`
|
||||
|
||||
| `+{*path}+`
|
||||
| Matches zero or more path segments and captures it as a variable named "path"
|
||||
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
|
||||
|
||||
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
|
||||
|
||||
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
|
||||
|
||||
`+"/{*path}/{name}/resources"+` is invalid as only a literal pattern is allowed right after `{*path}`.
|
||||
`+"/{*path}/project/{project}/resources"+` is allowed.
|
||||
|
||||
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|
||||
|
||||
|===
|
||||
|
||||
Captured URI variables can be accessed with `@PathVariable`. For example:
|
||||
|
||||
|
@ -217,10 +251,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]]
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
[[mvc-range]]
|
||||
= Range Requests
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
[.small]#xref:web/webflux/range.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC supports https://datatracker.ietf.org/doc/html/rfc9110#section-14[RFC 9110]
|
||||
range requests. For an overview, see the
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests[Ranger Requests]
|
||||
Mozilla guide.
|
||||
|
||||
The `Range` header is parsed and handled transparently in Spring MVC when an annotated
|
||||
controller returns a `Resource` or `ResponseEntity<Resource>`, or a functional endpoint
|
||||
xref:web/webmvc-functional.adoc#webmvc-fn-resources[serves a `Resource`]. `Range` header
|
||||
support is also transparently handled when serving
|
||||
xref:web/webmvc/mvc-config/static-resources.adoc[static resources].
|
||||
|
||||
TIP: The `Resource` must not be an `InputStreamResource` and with `ResponseEntity<Resource>`,
|
||||
the status of the response must be 200.
|
||||
|
||||
The underlying support is in the `HttpRange` class, which exposes methods to parse
|
||||
`Range` headers and split a `Resource` into a `List<ResourceRegion>` that in turn can be
|
||||
then written to the response via `ResourceRegionHttpMessageConverter`.
|
|
@ -1,8 +1,8 @@
|
|||
[[websocket-stomp-websocket-scope]]
|
||||
= WebSocket 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
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.jms.jmssending;
|
||||
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
import jakarta.jms.JMSException;
|
||||
import jakarta.jms.Message;
|
||||
import jakarta.jms.Queue;
|
||||
import jakarta.jms.Session;
|
||||
|
||||
import org.springframework.jms.core.MessageCreator;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
|
||||
public class JmsQueueSender {
|
||||
|
||||
private JmsTemplate jmsTemplate;
|
||||
private Queue queue;
|
||||
|
||||
public void setConnectionFactory(ConnectionFactory cf) {
|
||||
this.jmsTemplate = new JmsTemplate(cf);
|
||||
}
|
||||
|
||||
public void setQueue(Queue queue) {
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
public void simpleSend() {
|
||||
this.jmsTemplate.send(this.queue, new MessageCreator() {
|
||||
public Message createMessage(Session session) throws JMSException {
|
||||
return session.createTextMessage("hello queue world");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.jms.jmssendingconversion;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.jms.JMSException;
|
||||
import jakarta.jms.Message;
|
||||
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
import org.springframework.jms.core.MessagePostProcessor;
|
||||
|
||||
public class JmsSenderWithConversion {
|
||||
|
||||
private JmsTemplate jmsTemplate;
|
||||
|
||||
public void sendWithConversion() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("Name", "Mark");
|
||||
map.put("Age", 47);
|
||||
jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() {
|
||||
public Message postProcessMessage(Message message) throws JMSException {
|
||||
message.setIntProperty("AccountID", 1234);
|
||||
message.setJMSCorrelationID("123-00001");
|
||||
return message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.jms.jmssendingjmsclient;
|
||||
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
|
||||
import org.springframework.jms.core.JmsClient;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
public class JmsClientSample {
|
||||
|
||||
private final JmsClient jmsClient;
|
||||
|
||||
public JmsClientSample(ConnectionFactory connectionFactory) {
|
||||
// For custom options, use JmsClient.builder(ConnectionFactory)
|
||||
this.jmsClient = JmsClient.create(connectionFactory);
|
||||
}
|
||||
|
||||
public void sendWithConversion() {
|
||||
this.jmsClient.destination("myQueue")
|
||||
.withTimeToLive(1000)
|
||||
.send("myPayload"); // optionally with a headers Map next to the payload
|
||||
}
|
||||
|
||||
public void sendCustomMessage() {
|
||||
Message<?> message = MessageBuilder.withPayload("myPayload").build(); // optionally with headers
|
||||
this.jmsClient.destination("myQueue")
|
||||
.withTimeToLive(1000)
|
||||
.send(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.jms.jmssendingpostprocessor;
|
||||
|
||||
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
|
||||
import org.springframework.jms.core.JmsClient;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.core.MessagePostProcessor;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
public class JmsClientWithPostProcessor {
|
||||
|
||||
private final JmsClient jmsClient;
|
||||
|
||||
public JmsClientWithPostProcessor(ConnectionFactory connectionFactory) {
|
||||
this.jmsClient = JmsClient.builder(connectionFactory)
|
||||
.messagePostProcessor(new TenantIdMessageInterceptor("42"))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void sendWithPostProcessor() {
|
||||
this.jmsClient.destination("myQueue")
|
||||
.withTimeToLive(1000)
|
||||
.send("myPayload");
|
||||
}
|
||||
|
||||
static class TenantIdMessageInterceptor implements MessagePostProcessor {
|
||||
|
||||
private final String tenantId;
|
||||
|
||||
public TenantIdMessageInterceptor(String tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message<?> postProcessMessage(Message<?> message) {
|
||||
return MessageBuilder.fromMessage(message)
|
||||
.setHeader("tenantId", this.tenantId)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,37 +7,37 @@ javaPlatform {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.15.1"))
|
||||
api(platform("io.netty:netty-bom:4.2.2.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.0-M4"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.20.0-rc1"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.0-M2"))
|
||||
api(platform("io.netty:netty-bom:4.2.4.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.0-M6"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.27"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.0.0-rc-1"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3"))
|
||||
api(platform("org.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.eclipse.jetty:jetty-bom:12.1.0"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-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:5.13.4"))
|
||||
api(platform("org.mockito:mockito-bom:5.19.0"))
|
||||
api(platform("tools.jackson:jackson-bom:3.0.0-rc8"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.1")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.2")
|
||||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
api("com.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.protobuf:protobuf-java-util:4.32.0")
|
||||
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:mockwebserver:3.14.9")
|
||||
api("com.squareup.okhttp3:mockwebserver3:5.1.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.13.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.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")
|
||||
|
@ -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.orm:hibernate-core:7.1.0.Final")
|
||||
api("org.hibernate.validator:hibernate-validator:9.0.1.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.13.0")
|
||||
api("org.htmlunit:htmlunit:4.15.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.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.33.0")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.35.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.35.0")
|
||||
api("org.skyscreamer:jsonassert:2.0-rc1")
|
||||
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.0")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.0")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.3")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.3")
|
||||
api("org.yaml:snakeyaml:2.4")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,10 @@ org.gradle.jvmargs=-Xmx2048m
|
|||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=2.2.0
|
||||
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
|
|
@ -1,32 +0,0 @@
|
|||
tasks.findByName("dokkaHtmlPartial")?.configure {
|
||||
outputDirectory.set(new File(buildDir, "docs/kdoc"))
|
||||
dokkaSourceSets {
|
||||
configureEach {
|
||||
sourceRoots.setFrom(file("src/main/kotlin"))
|
||||
classpath.from(sourceSets["main"].runtimeClasspath)
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/"))
|
||||
packageListUrl.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/element-list"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://projectreactor.io/docs/core/release/api/"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://kotlin.github.io/kotlinx.coroutines/"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/"))
|
||||
packageListUrl.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,11 +15,6 @@ dependencies {
|
|||
jmh 'net.sf.jopt-simple:jopt-simple'
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("kotlin") {
|
||||
apply plugin: "org.jetbrains.dokka"
|
||||
apply from: "${rootDir}/gradle/docs-dokka.gradle"
|
||||
}
|
||||
|
||||
jmh {
|
||||
duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ import org.jspecify.annotations.Nullable;
|
|||
|
||||
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 abstract class AopConfigUtils {
|
|||
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 abstract class AopConfigUtils {
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -112,11 +112,13 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu
|
|||
|
||||
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 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSu
|
|||
protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
|
||||
ProxyFactory proxyFactory = new ProxyFactory();
|
||||
proxyFactory.copyFrom(this);
|
||||
proxyFactory.setFrozen(false);
|
||||
proxyFactory.setTarget(bean);
|
||||
return proxyFactory;
|
||||
}
|
||||
|
|
|
@ -694,7 +694,7 @@ class CglibAopProxy implements AopProxy, Serializable {
|
|||
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;
|
||||
|
|
|
@ -183,7 +183,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
|
|||
// 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 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
|
|||
|
||||
Object retVal;
|
||||
|
||||
if (this.advised.exposeProxy) {
|
||||
if (this.advised.isExposeProxy()) {
|
||||
// Make invocation available if necessary.
|
||||
oldProxy = AopContext.setCurrentProxy(proxy);
|
||||
setProxyContext = true;
|
||||
|
|
|
@ -18,6 +18,8 @@ package org.springframework.aop.framework;
|
|||
|
||||
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 class ProxyConfig implements Serializable {
|
|||
* 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 class ProxyConfig implements Serializable {
|
|||
* Return whether proxies should perform aggressive optimizations.
|
||||
*/
|
||||
public boolean isOptimize() {
|
||||
return this.optimize;
|
||||
return (this.optimize != null && this.optimize);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,7 +105,7 @@ public class ProxyConfig implements Serializable {
|
|||
* prevented from being cast to {@link Advised}.
|
||||
*/
|
||||
public boolean isOpaque() {
|
||||
return this.opaque;
|
||||
return (this.opaque != null && this.opaque);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -124,7 +126,7 @@ public class ProxyConfig implements Serializable {
|
|||
* each invocation.
|
||||
*/
|
||||
public boolean isExposeProxy() {
|
||||
return this.exposeProxy;
|
||||
return (this.exposeProxy != null && this.exposeProxy);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -141,7 +143,7 @@ public class ProxyConfig implements Serializable {
|
|||
* Return whether the config is frozen, and no advice changes can be made.
|
||||
*/
|
||||
public boolean isFrozen() {
|
||||
return this.frozen;
|
||||
return (this.frozen != null && this.frozen);
|
||||
}
|
||||
|
||||
|
||||
|
@ -153,9 +155,34 @@ public class ProxyConfig implements Serializable {
|
|||
Assert.notNull(other, "Other ProxyConfig object must not be null");
|
||||
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
|
||||
|
|
|
@ -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<Object, Boolean> advisedBeans = new ConcurrentHashMap<>(256);
|
||||
|
||||
|
||||
/**
|
||||
* Set whether the proxy should be frozen, preventing advice
|
||||
* from being added to it once it is created.
|
||||
* <p>Overridden from the superclass to prevent the proxy configuration
|
||||
* from being frozen before the proxy is created.
|
||||
*/
|
||||
@Override
|
||||
public void setFrozen(boolean frozen) {
|
||||
this.freezeProxy = frozen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFrozen() {
|
||||
return this.freezeProxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the {@link AdvisorAdapterRegistry} to use.
|
||||
* <p>Default is the global {@link AdvisorAdapterRegistry}.
|
||||
|
@ -206,6 +184,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
|||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) {
|
||||
this.beanFactory = beanFactory;
|
||||
AutoProxyUtils.applyDefaultProxyConfig(this, beanFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -471,6 +450,24 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
|||
|
||||
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 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
|||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No proxyTargetClass flag enforced, let's apply our default checks...
|
||||
if (shouldProxyTargetClass(beanClass, beanName)) {
|
||||
proxyFactory.setProxyTargetClass(true);
|
||||
}
|
||||
else {
|
||||
evaluateProxyInterfaces(beanClass, proxyFactory);
|
||||
}
|
||||
}
|
||||
|
||||
Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
|
||||
proxyFactory.addAdvisors(advisors);
|
||||
proxyFactory.setTargetSource(targetSource);
|
||||
customizeProxyFactory(proxyFactory);
|
||||
|
||||
proxyFactory.setFrozen(this.freezeProxy);
|
||||
proxyFactory.setFrozen(isFrozen());
|
||||
if (advisorsPreFiltered()) {
|
||||
proxyFactory.setPreFiltered(true);
|
||||
}
|
||||
|
|
|
@ -25,9 +25,9 @@ import org.springframework.beans.factory.BeanFactoryAware;
|
|||
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 @@ public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends Abst
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ package org.springframework.aop.framework.autoproxy;
|
|||
|
||||
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 @@ import org.springframework.util.StringUtils;
|
|||
* @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) {
|
||||
|
|
|
@ -20,8 +20,6 @@ import java.beans.BeanInfo;
|
|||
import java.beans.IntrospectionException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
/**
|
||||
|
@ -44,7 +42,7 @@ import org.springframework.core.Ordered;
|
|||
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);
|
||||
}
|
||||
|
|
|
@ -514,8 +514,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
|||
* to check whether the bean with the given name matches the specified type. Allow
|
||||
* 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
|
||||
|
|
|
@ -60,6 +60,7 @@ import org.springframework.beans.factory.BeanFactoryAware;
|
|||
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;
|
||||
|
@ -191,8 +192,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
|||
/** Map from bean name to merged BeanDefinitionHolder. */
|
||||
private final Map<String, BeanDefinitionHolder> mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256);
|
||||
|
||||
/** Set of bean definition names with a primary marker. */
|
||||
private final Set<String> primaryBeanNames = ConcurrentHashMap.newKeySet(16);
|
||||
/** Map of bean definition names with a primary marker plus corresponding type. */
|
||||
private final Map<String, Class<?>> primaryBeanNamesWithType = new ConcurrentHashMap<>(16);
|
||||
|
||||
/** Map of singleton and non-singleton bean names, keyed by dependency type. */
|
||||
private final Map<Class<?>, String[]> allBeanNamesByType = new ConcurrentHashMap<>(64);
|
||||
|
@ -1024,7 +1025,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
|||
protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) {
|
||||
super.cacheMergedBeanDefinition(mbd, beanName);
|
||||
if (mbd.isPrimary()) {
|
||||
this.primaryBeanNames.add(beanName);
|
||||
this.primaryBeanNamesWithType.put(beanName, Void.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1309,7 +1310,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
|||
|
||||
// Cache a primary marker for the given bean.
|
||||
if (beanDefinition.isPrimary()) {
|
||||
this.primaryBeanNames.add(beanName);
|
||||
this.primaryBeanNamesWithType.put(beanName, Void.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1401,7 +1402,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
|||
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 +1452,27 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addSingleton(String beanName, Object singletonObject) {
|
||||
super.addSingleton(beanName, singletonObject);
|
||||
|
||||
Predicate<Class<?>> filter = (beanType -> beanType != Object.class && beanType.isInstance(singletonObject));
|
||||
this.allBeanNamesByType.keySet().removeIf(filter);
|
||||
this.singletonBeanNamesByType.keySet().removeIf(filter);
|
||||
|
||||
if (this.primaryBeanNamesWithType.containsKey(beanName) && singletonObject.getClass() != NullBean.class) {
|
||||
Class<?> beanType = (singletonObject instanceof FactoryBean<?> fb ?
|
||||
getTypeForFactoryBean(fb) : singletonObject.getClass());
|
||||
if (beanType != null) {
|
||||
this.primaryBeanNamesWithType.put(beanName, beanType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
|
||||
super.registerSingleton(beanName, singletonObject);
|
||||
updateManualSingletonNames(set -> set.add(beanName), set -> !this.beanDefinitionMap.containsKey(beanName));
|
||||
clearByTypeCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2238,8 +2255,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
|||
* 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<String, Class<?>> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ public class PathEditor extends PropertyEditorSupport {
|
|||
if (resource == null) {
|
||||
setValue(null);
|
||||
}
|
||||
else if (nioPathCandidate && !resource.exists()) {
|
||||
else if (nioPathCandidate && (!resource.isFile() || !resource.exists())) {
|
||||
setValue(Paths.get(text).normalize());
|
||||
}
|
||||
else {
|
||||
|
|
|
@ -3211,6 +3211,29 @@ class DefaultListableBeanFactoryTests {
|
|||
assertThat(holder.getNonPublicEnum()).isEqualTo(NonPublicEnum.VALUE_1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mostSpecificCacheEntryForTypeMatching() {
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition();
|
||||
bd1.setFactoryBeanName("config");
|
||||
bd1.setFactoryMethodName("create");
|
||||
lbf.registerBeanDefinition("config", new RootBeanDefinition(BeanWithFactoryMethod.class));
|
||||
lbf.registerBeanDefinition("bd1", bd1);
|
||||
lbf.registerBeanDefinition("bd2", new RootBeanDefinition(NestedTestBean.class));
|
||||
lbf.freezeConfiguration();
|
||||
|
||||
String[] allBeanNames = lbf.getBeanNamesForType(Object.class);
|
||||
String[] nestedBeanNames = lbf.getBeanNamesForType(NestedTestBean.class);
|
||||
assertThat(lbf.getType("bd1")).isEqualTo(TestBean.class);
|
||||
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
|
||||
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).isEmpty();
|
||||
lbf.getBean("bd1");
|
||||
assertThat(lbf.getType("bd1")).isEqualTo(DerivedTestBean.class);
|
||||
assertThat(lbf.getBeanNamesForType(TestBean.class)).containsExactly("bd1");
|
||||
assertThat(lbf.getBeanNamesForType(DerivedTestBean.class)).containsExactly("bd1");
|
||||
assertThat(lbf.getBeanNamesForType(NestedTestBean.class)).isSameAs(nestedBeanNames);
|
||||
assertThat(lbf.getBeanNamesForType(Object.class)).isSameAs(allBeanNames);
|
||||
}
|
||||
|
||||
|
||||
private int registerBeanDefinitions(Properties p) {
|
||||
return registerBeanDefinitions(p, null);
|
||||
|
@ -3427,7 +3450,7 @@ class DefaultListableBeanFactoryTests {
|
|||
}
|
||||
|
||||
public TestBean create() {
|
||||
TestBean tb = new TestBean();
|
||||
DerivedTestBean tb = new DerivedTestBean();
|
||||
tb.setName(this.name);
|
||||
return tb;
|
||||
}
|
||||
|
@ -3655,11 +3678,11 @@ class DefaultListableBeanFactoryTests {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.quartz.utils.DBConnectionManager;
|
|||
import org.springframework.jdbc.datasource.DataSourceUtils;
|
||||
import org.springframework.jdbc.support.JdbcUtils;
|
||||
import org.springframework.jdbc.support.MetaDataAccessException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed
|
||||
|
@ -88,6 +89,8 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
|
|||
|
||||
private @Nullable DataSource dataSource;
|
||||
|
||||
private @Nullable DataSource nonTransactionalDataSource;
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullAway") // Dataflow analysis limitation
|
||||
|
@ -98,11 +101,40 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
|
|||
throw new SchedulerConfigException("No local DataSource found for configuration - " +
|
||||
"'dataSource' property must be set on SchedulerFactoryBean");
|
||||
}
|
||||
// Non-transactional DataSource is optional: fall back to default
|
||||
// DataSource if not explicitly specified.
|
||||
this.nonTransactionalDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
|
||||
|
||||
// Configure transactional connection settings for Quartz.
|
||||
// Configure connection settings for Quartz.
|
||||
setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName());
|
||||
setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
|
||||
setDontSetAutoCommitFalse(true);
|
||||
|
||||
initializeConnectionProvider();
|
||||
|
||||
// No, if HSQL is the platform, we really don't want to use locks...
|
||||
try {
|
||||
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
|
||||
DatabaseMetaData::getDatabaseProductName);
|
||||
productName = JdbcUtils.commonDatabaseName(productName);
|
||||
if (productName != null && productName.toLowerCase(Locale.ROOT).contains("hsql")) {
|
||||
setUseDBLocks(false);
|
||||
setLockHandler(new SimpleSemaphore());
|
||||
}
|
||||
}
|
||||
catch (MetaDataAccessException ex) {
|
||||
logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
|
||||
}
|
||||
|
||||
super.initialize(loadHelper, signaler);
|
||||
}
|
||||
|
||||
void initializeConnectionProvider() {
|
||||
final DataSource dataSourceToUse = this.dataSource;
|
||||
Assert.state(dataSourceToUse != null, "DataSource must not be null");
|
||||
final DataSource nonTxDataSourceToUse =
|
||||
(this.nonTransactionalDataSource != null ? this.nonTransactionalDataSource : dataSourceToUse);
|
||||
|
||||
// Register transactional ConnectionProvider for Quartz.
|
||||
DBConnectionManager.getInstance().addConnectionProvider(
|
||||
TX_DATA_SOURCE_PREFIX + getInstanceName(),
|
||||
|
@ -110,7 +142,7 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
|
|||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
// Return a transactional Connection, if any.
|
||||
return DataSourceUtils.doGetConnection(dataSource);
|
||||
return DataSourceUtils.doGetConnection(dataSourceToUse);
|
||||
}
|
||||
@Override
|
||||
public void shutdown() {
|
||||
|
@ -123,14 +155,6 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
|
|||
}
|
||||
);
|
||||
|
||||
// Non-transactional DataSource is optional: fall back to default
|
||||
// DataSource if not explicitly specified.
|
||||
DataSource nonTxDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
|
||||
final DataSource nonTxDataSourceToUse = (nonTxDataSource != null ? nonTxDataSource : this.dataSource);
|
||||
|
||||
// Configure non-transactional connection settings for Quartz.
|
||||
setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
|
||||
|
||||
// Register non-transactional ConnectionProvider for Quartz.
|
||||
DBConnectionManager.getInstance().addConnectionProvider(
|
||||
NON_TX_DATA_SOURCE_PREFIX + getInstanceName(),
|
||||
|
@ -150,23 +174,6 @@ public class LocalDataSourceJobStore extends JobStoreCMT {
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
// No, if HSQL is the platform, we really don't want to use locks...
|
||||
try {
|
||||
String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource,
|
||||
DatabaseMetaData::getDatabaseProductName);
|
||||
productName = JdbcUtils.commonDatabaseName(productName);
|
||||
if (productName != null && productName.toLowerCase(Locale.ROOT).contains("hsql")) {
|
||||
setUseDBLocks(false);
|
||||
setLockHandler(new SimpleSemaphore());
|
||||
}
|
||||
}
|
||||
catch (MetaDataAccessException ex) {
|
||||
logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
|
||||
}
|
||||
|
||||
super.initialize(loadHelper, signaler);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -28,6 +28,8 @@ import org.jspecify.annotations.Nullable;
|
|||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerException;
|
||||
import org.quartz.SchedulerFactory;
|
||||
import org.quartz.core.QuartzScheduler;
|
||||
import org.quartz.core.QuartzSchedulerResources;
|
||||
import org.quartz.impl.RemoteScheduler;
|
||||
import org.quartz.impl.SchedulerRepository;
|
||||
import org.quartz.impl.StdSchedulerFactory;
|
||||
|
@ -165,7 +167,7 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
|
|||
|
||||
private @Nullable SchedulerFactory schedulerFactory;
|
||||
|
||||
private Class<? extends SchedulerFactory> schedulerFactoryClass = StdSchedulerFactory.class;
|
||||
private Class<? extends SchedulerFactory> schedulerFactoryClass = LocalSchedulerFactory.class;
|
||||
|
||||
private @Nullable String schedulerName;
|
||||
|
||||
|
@ -203,6 +205,8 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
|
|||
|
||||
private @Nullable Scheduler scheduler;
|
||||
|
||||
private @Nullable LocalDataSourceJobStore jobStore;
|
||||
|
||||
|
||||
/**
|
||||
* Set an external Quartz {@link SchedulerFactory} instance to use.
|
||||
|
@ -223,11 +227,12 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
|
|||
|
||||
/**
|
||||
* Set the Quartz {@link SchedulerFactory} implementation to use.
|
||||
* <p>Default is the {@link StdSchedulerFactory} class, reading in the standard
|
||||
* {@code quartz.properties} from {@code quartz.jar}. For applying custom Quartz
|
||||
* properties, specify {@link #setConfigLocation "configLocation"} and/or
|
||||
* {@link #setQuartzProperties "quartzProperties"} etc on this local
|
||||
* {@code SchedulerFactoryBean} instance.
|
||||
* <p>Default is a Spring-internal subclass of the {@link StdSchedulerFactory}
|
||||
* class, reading in the standard {@code quartz.properties} from
|
||||
* {@code quartz.jar}. For applying custom Quartz properties,
|
||||
* specify {@link #setConfigLocation "configLocation"} and/or
|
||||
* {@link #setQuartzProperties "quartzProperties"} etc on this
|
||||
* local {@code SchedulerFactoryBean} instance.
|
||||
* @see org.quartz.impl.StdSchedulerFactory
|
||||
* @see #setConfigLocation
|
||||
* @see #setQuartzProperties
|
||||
|
@ -508,8 +513,9 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
|
|||
private SchedulerFactory prepareSchedulerFactory() throws SchedulerException, IOException {
|
||||
SchedulerFactory schedulerFactory = this.schedulerFactory;
|
||||
if (schedulerFactory == null) {
|
||||
// Create local SchedulerFactory instance (typically a StdSchedulerFactory)
|
||||
schedulerFactory = BeanUtils.instantiateClass(this.schedulerFactoryClass);
|
||||
// Create local SchedulerFactory instance (typically a LocalSchedulerFactory)
|
||||
schedulerFactory = (this.schedulerFactoryClass == LocalSchedulerFactory.class ?
|
||||
new LocalSchedulerFactory() : BeanUtils.instantiateClass(this.schedulerFactoryClass));
|
||||
if (schedulerFactory instanceof StdSchedulerFactory stdSchedulerFactory) {
|
||||
initSchedulerFactory(stdSchedulerFactory);
|
||||
}
|
||||
|
@ -778,6 +784,9 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
|
|||
@Override
|
||||
public void start() throws SchedulingException {
|
||||
if (this.scheduler != null) {
|
||||
if (this.jobStore != null) {
|
||||
this.jobStore.initializeConnectionProvider();
|
||||
}
|
||||
try {
|
||||
startScheduler(this.scheduler, this.startupDelay);
|
||||
}
|
||||
|
@ -829,4 +838,16 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private class LocalSchedulerFactory extends StdSchedulerFactory {
|
||||
|
||||
@Override
|
||||
protected Scheduler instantiate(QuartzSchedulerResources rsrcs, QuartzScheduler qs) {
|
||||
if (rsrcs.getJobStore() instanceof LocalDataSourceJobStore ldsjs) {
|
||||
SchedulerFactoryBean.this.jobStore = ldsjs;
|
||||
}
|
||||
return super.instantiate(rsrcs, qs);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -391,6 +391,7 @@ class QuartzSupportTests {
|
|||
try (ClassPathXmlApplicationContext ctx = context("databasePersistence.xml")) {
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(ctx.getBean(DataSource.class));
|
||||
assertThat(jdbcTemplate.queryForList("SELECT * FROM qrtz_triggers").isEmpty()).as("No triggers were persisted").isFalse();
|
||||
ctx.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,28 +5,28 @@
|
|||
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
|
||||
<property name="triggers" ref="trigger" />
|
||||
<property name="dataSource" ref="dataSource" />
|
||||
<property name="triggers" ref="trigger"/>
|
||||
<property name="dataSource" ref="dataSource"/>
|
||||
</bean>
|
||||
|
||||
<bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
|
||||
<property name="repeatInterval" value="1000" />
|
||||
<property name="repeatCount" value="1" />
|
||||
<property name="repeatInterval" value="1000"/>
|
||||
<property name="repeatCount" value="1"/>
|
||||
<property name="jobDetail">
|
||||
<bean class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
|
||||
<property name="jobDataAsMap">
|
||||
<map>
|
||||
<entry key="param" value="10" />
|
||||
<entry key="param" value="10"/>
|
||||
</map>
|
||||
</property>
|
||||
<property name="jobClass" value="org.springframework.scheduling.quartz.QuartzSupportTests$DummyJob" />
|
||||
<property name="durability" value="true" />
|
||||
<property name="jobClass" value="org.springframework.scheduling.quartz.QuartzSupportTests$DummyJob"/>
|
||||
<property name="durability" value="true"/>
|
||||
</bean>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<jdbc:embedded-database id="dataSource" type="HSQL">
|
||||
<jdbc:script location="org/springframework/scheduling/quartz/quartz-hsql.sql" />
|
||||
<jdbc:script location="org/springframework/scheduling/quartz/quartz-hsql.sql"/>
|
||||
</jdbc:embedded-database>
|
||||
|
||||
</beans>
|
||||
|
|
|
@ -48,7 +48,7 @@ class CacheEvaluationContext extends MethodBasedEvaluationContext {
|
|||
private final Set<String> unavailableVariables = new HashSet<>(1);
|
||||
|
||||
|
||||
CacheEvaluationContext(Object rootObject, Method method, @Nullable Object[] arguments,
|
||||
CacheEvaluationContext(@Nullable Object rootObject, Method method, @Nullable Object[] arguments,
|
||||
ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
|
||||
super(rootObject, method, arguments, parameterNameDiscoverer);
|
||||
|
|
|
@ -221,16 +221,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.
|
||||
|
|
|
@ -44,6 +44,15 @@ public interface LifecycleProcessor extends Lifecycle {
|
|||
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.
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
* <p>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.
|
||||
* <p>A value of {@code false} indicates that the component prefers to
|
||||
* be skipped in a pause scenario, neither receiving a {@link #stop()}
|
||||
* call nor a subsequent {@link #start()} call, analogous to a plain
|
||||
* {@link Lifecycle} implementation. It will only receive a {@link #stop()}
|
||||
* call on close and on explicit context-wide stopping but not on pause.
|
||||
* <p>The default implementation returns {@code true}.
|
||||
* @since 7.0
|
||||
* @see #stop()
|
||||
* @see LifecycleProcessor#onPause()
|
||||
* @see LifecycleProcessor#onClose()
|
||||
* @see ConfigurableApplicationContext#pause()
|
||||
* @see ConfigurableApplicationContext#close()
|
||||
*/
|
||||
default boolean isPauseable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a Lifecycle component must stop if it is currently running.
|
||||
* <p>The provided callback is used by the {@link LifecycleProcessor} to support
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.util.function.Predicate;
|
|||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.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;
|
||||
|
@ -258,6 +259,20 @@ public abstract class AnnotationConfigUtils {
|
|||
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(
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.annotation;
|
||||
|
||||
/**
|
||||
* Common enum for indicating a desired proxy type.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0
|
||||
* @see Proxyable#value()
|
||||
*/
|
||||
public enum ProxyType {
|
||||
|
||||
/**
|
||||
* Default is a JDK dynamic proxy, or potentially a class-based CGLIB proxy
|
||||
* when globally configured.
|
||||
*/
|
||||
DEFAULT,
|
||||
|
||||
/**
|
||||
* Suggest a JDK dynamic proxy implementing <i>all</i> interfaces exposed by
|
||||
* the class of the target object. Overrides a globally configured default.
|
||||
*/
|
||||
INTERFACES,
|
||||
|
||||
/**
|
||||
* Suggest a class-based CGLIB proxy. Overrides a globally configured default.
|
||||
*/
|
||||
TARGET_CLASS
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Common annotation for suggesting a specific proxy type for a {@link Bean @Bean}
|
||||
* method or {@link org.springframework.stereotype.Component @Component} class,
|
||||
* overriding a globally configured default.
|
||||
*
|
||||
* <p>Only actually applying in case of a bean actually getting auto-proxied in
|
||||
* the first place. Actual auto-proxying is dependent on external configuration.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0
|
||||
* @see org.springframework.aop.framework.autoproxy.AutoProxyUtils#PRESERVE_TARGET_CLASS_ATTRIBUTE
|
||||
* @see org.springframework.aop.framework.autoproxy.AutoProxyUtils#EXPOSED_INTERFACES_ATTRIBUTE
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Proxyable {
|
||||
|
||||
/**
|
||||
* Suggest a specific proxy type, either {@link ProxyType#INTERFACES} for
|
||||
* a JDK dynamic proxy or {@link ProxyType#TARGET_CLASS} for a CGLIB proxy,
|
||||
* overriding a globally configured default.
|
||||
*/
|
||||
ProxyType value() default ProxyType.DEFAULT;
|
||||
|
||||
/**
|
||||
* Suggest a JDK dynamic proxy with specific interfaces to expose, overriding
|
||||
* a globally configured default.
|
||||
* <p>Only taken into account if {@link #value()} is not {@link ProxyType#TARGET_CLASS}.
|
||||
*/
|
||||
Class<?>[] interfaces() default {};
|
||||
|
||||
|
||||
}
|
|
@ -51,9 +51,9 @@ import org.springframework.core.annotation.AliasFor;
|
|||
* ignored.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 6.2
|
||||
* @see Reflective @Reflective
|
||||
* @see RegisterReflection @RegisterReflection
|
||||
* @since 6.2
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.event;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
/**
|
||||
* Event raised when an {@code ApplicationContext} gets paused.
|
||||
*
|
||||
* <p>Note that {@code ContextPausedEvent} is a specialization of
|
||||
* {@link ContextStoppedEvent}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0
|
||||
* @see ConfigurableApplicationContext#pause()
|
||||
* @see ContextRestartedEvent
|
||||
* @see ContextStoppedEvent
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ContextPausedEvent extends ContextStoppedEvent {
|
||||
|
||||
/**
|
||||
* Create a new {@code ContextPausedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been paused
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
public ContextPausedEvent(ApplicationContext source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.context.event;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
/**
|
||||
* Event raised when an {@code ApplicationContext} gets restarted.
|
||||
|
@ -26,8 +27,9 @@ import org.springframework.context.ApplicationContext;
|
|||
*
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
* @see ConfigurableApplicationContext#restart()
|
||||
* @see ContextPausedEvent
|
||||
* @see ContextStartedEvent
|
||||
* @see ContextStoppedEvent
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ContextRestartedEvent extends ContextStartedEvent {
|
||||
|
|
|
@ -53,7 +53,7 @@ public class MethodBasedEvaluationContext extends StandardEvaluationContext {
|
|||
private boolean argumentsLoaded = false;
|
||||
|
||||
|
||||
public MethodBasedEvaluationContext(Object rootObject, Method method, @Nullable Object[] arguments,
|
||||
public MethodBasedEvaluationContext(@Nullable Object rootObject, Method method, @Nullable Object[] arguments,
|
||||
ParameterNameDiscoverer parameterNameDiscoverer) {
|
||||
|
||||
super(rootObject);
|
||||
|
|
|
@ -66,6 +66,7 @@ import org.springframework.context.PayloadApplicationEvent;
|
|||
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;
|
||||
|
@ -1486,17 +1487,17 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
|||
//---------------------------------------------------------------------
|
||||
|
||||
@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 +1556,12 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
|||
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());
|
||||
|
|
|
@ -134,7 +134,7 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
|||
|
||||
|
||||
@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 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
|||
}
|
||||
|
||||
@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 abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
|||
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 abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
|||
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 abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
|||
* @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 abstract class AbstractMessageSource extends MessageSourceSupport impleme
|
|||
* @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);
|
||||
}
|
||||
|
|
|
@ -287,7 +287,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
*/
|
||||
@Override
|
||||
public void stop() {
|
||||
stopBeans();
|
||||
stopBeans(false);
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
|
@ -308,7 +308,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
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 class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
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 class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
void stopForRestart() {
|
||||
if (this.running) {
|
||||
this.stoppedBeans = ConcurrentHashMap.newKeySet();
|
||||
stopBeans();
|
||||
stopBeans(false);
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
@ -361,7 +369,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
lifecycleBeans.forEach((beanName, bean) -> {
|
||||
if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
|
||||
int startupPhase = getPhase(bean);
|
||||
phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly))
|
||||
phases.computeIfAbsent(
|
||||
startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly, false))
|
||||
.add(beanName, bean);
|
||||
}
|
||||
});
|
||||
|
@ -424,13 +433,14 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
(!(bean instanceof SmartLifecycle smartLifecycle) || smartLifecycle.isAutoStartup()));
|
||||
}
|
||||
|
||||
private void stopBeans() {
|
||||
private void stopBeans(boolean pauseableOnly) {
|
||||
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
|
||||
Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
|
||||
|
||||
lifecycleBeans.forEach((beanName, bean) -> {
|
||||
int shutdownPhase = getPhase(bean);
|
||||
phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false))
|
||||
phases.computeIfAbsent(
|
||||
shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false, pauseableOnly))
|
||||
.add(beanName, bean);
|
||||
});
|
||||
|
||||
|
@ -446,13 +456,13 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
* @param beanName the name of the bean to stop
|
||||
*/
|
||||
private void doStop(Map<String, ? extends Lifecycle> lifecycleBeans, final String beanName,
|
||||
final CountDownLatch latch, final Set<String> countDownBeanNames) {
|
||||
boolean pauseableOnly, final CountDownLatch latch, final Set<String> countDownBeanNames) {
|
||||
|
||||
Lifecycle bean = lifecycleBeans.remove(beanName);
|
||||
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 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
stoppedBeans.add(beanName);
|
||||
}
|
||||
if (bean instanceof SmartLifecycle smartLifecycle) {
|
||||
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");
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Don't wait for beans that aren't pauseable...
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (!pauseableOnly) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Stopping bean '" + beanName + "' of type [" +
|
||||
bean.getClass().getName() + "]");
|
||||
|
@ -562,14 +578,19 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
|
||||
private final boolean autoStartupOnly;
|
||||
|
||||
private final boolean pauseableOnly;
|
||||
|
||||
private final List<LifecycleGroupMember> members = new ArrayList<>();
|
||||
|
||||
private int smartMemberCount;
|
||||
|
||||
public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans, boolean autoStartupOnly) {
|
||||
public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans,
|
||||
boolean autoStartupOnly, boolean pauseableOnly) {
|
||||
|
||||
this.phase = phase;
|
||||
this.lifecycleBeans = lifecycleBeans;
|
||||
this.autoStartupOnly = autoStartupOnly;
|
||||
this.pauseableOnly = pauseableOnly;
|
||||
}
|
||||
|
||||
public void add(String name, Lifecycle bean) {
|
||||
|
@ -621,7 +642,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
|||
Set<String> lifecycleBeanNames = new HashSet<>(this.lifecycleBeans.keySet());
|
||||
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
|
||||
|
|
|
@ -53,7 +53,7 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
|
|||
|
||||
|
||||
@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 @@ public class DelegatingMessageSource extends MessageSourceSupport implements Hie
|
|||
}
|
||||
|
||||
@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 class DelegatingMessageSource extends MessageSourceSupport implements Hie
|
|||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ public abstract class MessageSourceSupport {
|
|||
* @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 @@ public abstract class MessageSourceSupport {
|
|||
* @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 @@ public abstract class MessageSourceSupport {
|
|||
* @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 @@ public abstract class MessageSourceSupport {
|
|||
* @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]);
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import reactor.core.publisher.Flux;
|
|||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
|
||||
import org.springframework.aop.ProxyMethodInvocation;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.retry.RetryException;
|
||||
|
@ -103,7 +104,8 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
|||
return retryTemplate.execute(new Retryable<>() {
|
||||
@Override
|
||||
public @Nullable Object execute() throws Throwable {
|
||||
return invocation.proceed();
|
||||
return (invocation instanceof ProxyMethodInvocation pmi ?
|
||||
pmi.invocableClone().proceed() : invocation.proceed());
|
||||
}
|
||||
@Override
|
||||
public String getName() {
|
||||
|
@ -112,8 +114,7 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
|||
});
|
||||
}
|
||||
catch (RetryException ex) {
|
||||
Throwable cause = ex.getCause();
|
||||
throw (cause != null ? cause : new IllegalStateException(ex.getMessage(), ex));
|
||||
throw ex.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -360,7 +360,7 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements
|
|||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.triggerLifecycle.isRunning();
|
||||
return (this.triggerLifecycle.isRunning() || this.fixedDelayLifecycle.isRunning());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.springframework.context.ConfigurableApplicationContext;
|
|||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.support.AbstractApplicationContext;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
|
@ -43,7 +44,7 @@ import org.springframework.util.Assert;
|
|||
* @author Mark Fisher
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
@Service @Lazy @DependsOn("myNamedComponent")
|
||||
@Service @Primary @Lazy @DependsOn("myNamedComponent")
|
||||
public abstract class FooServiceImpl implements FooService {
|
||||
|
||||
// Just to test ASM5's bytecode parsing of INVOKESPECIAL/STATIC on interfaces
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package example.scannable;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.springframework.context.annotation.Proxyable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
@Service @Proxyable(interfaces = FooService.class)
|
||||
public class OtherFooService implements FooService {
|
||||
|
||||
@Override
|
||||
public String foo(int id) {
|
||||
return "" + id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> asyncFoo(int id) {
|
||||
return CompletableFuture.completedFuture("" + id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitCalled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -60,12 +60,6 @@ class CacheOperationExpressionEvaluatorTests {
|
|||
private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource();
|
||||
|
||||
|
||||
private Collection<CacheOperation> getOps(String name) {
|
||||
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
|
||||
return this.source.getCacheOperations(method, AnnotatedClass.class);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testMultipleCachingSource() {
|
||||
Collection<CacheOperation> ops = getOps("multipleCaching");
|
||||
|
@ -144,6 +138,12 @@ class CacheOperationExpressionEvaluatorTests {
|
|||
assertThat(value).isEqualTo(String.class.getName());
|
||||
}
|
||||
|
||||
|
||||
private Collection<CacheOperation> getOps(String name) {
|
||||
Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name, Object.class, Object.class);
|
||||
return this.source.getCacheOperations(method, AnnotatedClass.class);
|
||||
}
|
||||
|
||||
private EvaluationContext createEvaluationContext(Object result) {
|
||||
return createEvaluationContext(result, null);
|
||||
}
|
||||
|
|
|
@ -104,6 +104,7 @@ class CachePutEvaluationTests {
|
|||
assertThat(this.cache.get(anotherValue + 100).get()).as("Wrong value for @CachePut key").isEqualTo(anotherValue);
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
static class Config implements CachingConfigurer {
|
||||
|
@ -121,8 +122,10 @@ class CachePutEvaluationTests {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@CacheConfig("test")
|
||||
public static class SimpleService {
|
||||
|
||||
private AtomicLong counter = new AtomicLong();
|
||||
|
||||
/**
|
||||
|
@ -144,4 +147,5 @@ class CachePutEvaluationTests {
|
|||
return this.counter.getAndIncrement();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import example.scannable.JakartaNamedComponent;
|
|||
import example.scannable.MessageBean;
|
||||
import example.scannable.NamedComponent;
|
||||
import example.scannable.NamedStubDao;
|
||||
import example.scannable.OtherFooService;
|
||||
import example.scannable.ScopedProxyTestBean;
|
||||
import example.scannable.ServiceInvocationCounter;
|
||||
import example.scannable.StubFooDao;
|
||||
|
@ -85,13 +86,13 @@ class ClassPathScanningCandidateComponentProviderTests {
|
|||
|
||||
private static final Set<Class<?>> springComponents = Set.of(
|
||||
DefaultNamedComponent.class,
|
||||
NamedComponent.class,
|
||||
FooServiceImpl.class,
|
||||
StubFooDao.class,
|
||||
NamedComponent.class,
|
||||
NamedStubDao.class,
|
||||
OtherFooService.class,
|
||||
ServiceInvocationCounter.class,
|
||||
BarComponent.class
|
||||
);
|
||||
StubFooDao.class,
|
||||
BarComponent.class);
|
||||
|
||||
|
||||
@Test
|
||||
|
@ -213,7 +214,8 @@ class ClassPathScanningCandidateComponentProviderTests {
|
|||
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
|
||||
assertScannedBeanDefinitions(candidates);
|
||||
// Interfaces/Abstract class are filtered out automatically.
|
||||
assertBeanTypes(candidates, AutowiredQualifierFooService.class, FooServiceImpl.class, ScopedProxyTestBean.class);
|
||||
assertBeanTypes(candidates,
|
||||
AutowiredQualifierFooService.class, FooServiceImpl.class, OtherFooService.class, ScopedProxyTestBean.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -237,7 +239,8 @@ class ClassPathScanningCandidateComponentProviderTests {
|
|||
provider.addExcludeFilter(new AnnotationTypeFilter(Repository.class));
|
||||
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
|
||||
assertScannedBeanDefinitions(candidates);
|
||||
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
|
||||
assertBeanTypes(candidates,
|
||||
NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -282,7 +285,8 @@ class ClassPathScanningCandidateComponentProviderTests {
|
|||
private void testExclude(ClassPathScanningCandidateComponentProvider provider) {
|
||||
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
|
||||
assertScannedBeanDefinitions(candidates);
|
||||
assertBeanTypes(candidates, FooServiceImpl.class, StubFooDao.class, ServiceInvocationCounter.class,
|
||||
assertBeanTypes(candidates,
|
||||
FooServiceImpl.class, OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class,
|
||||
BarComponent.class);
|
||||
}
|
||||
|
||||
|
@ -301,7 +305,8 @@ class ClassPathScanningCandidateComponentProviderTests {
|
|||
provider.addExcludeFilter(new AnnotationTypeFilter(Service.class));
|
||||
provider.addExcludeFilter(new AnnotationTypeFilter(Controller.class));
|
||||
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
|
||||
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
|
||||
assertBeanTypes(candidates,
|
||||
NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -334,8 +339,9 @@ class ClassPathScanningCandidateComponentProviderTests {
|
|||
provider.addIncludeFilter(new AnnotationTypeFilter(Component.class));
|
||||
provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class));
|
||||
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
|
||||
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, FooServiceImpl.class,
|
||||
BarComponent.class, DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class);
|
||||
assertBeanTypes(candidates,
|
||||
DefaultNamedComponent.class, FooServiceImpl.class, NamedComponent.class, NamedStubDao.class,
|
||||
OtherFooService.class, ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -345,8 +351,9 @@ class ClassPathScanningCandidateComponentProviderTests {
|
|||
provider.addIncludeFilter(new AssignableTypeFilter(FooServiceImpl.class));
|
||||
provider.addExcludeFilter(new AssignableTypeFilter(FooService.class));
|
||||
Set<BeanDefinition> candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE);
|
||||
assertBeanTypes(candidates, NamedComponent.class, ServiceInvocationCounter.class, BarComponent.class,
|
||||
DefaultNamedComponent.class, NamedStubDao.class, StubFooDao.class);
|
||||
assertBeanTypes(candidates,
|
||||
DefaultNamedComponent.class, NamedComponent.class, NamedStubDao.class,
|
||||
ServiceInvocationCounter.class, StubFooDao.class, BarComponent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -47,6 +47,7 @@ class EnableAspectJAutoProxyTests {
|
|||
|
||||
aspectIsApplied(ctx);
|
||||
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean(FooService.class))).isTrue();
|
||||
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue();
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
|
@ -56,6 +57,7 @@ class EnableAspectJAutoProxyTests {
|
|||
|
||||
aspectIsApplied(ctx);
|
||||
assertThat(AopUtils.isCglibProxy(ctx.getBean(FooService.class))).isTrue();
|
||||
assertThat(AopUtils.isJdkDynamicProxy(ctx.getBean("otherFooService"))).isTrue();
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
|
@ -124,7 +126,7 @@ class EnableAspectJAutoProxyTests {
|
|||
}
|
||||
|
||||
|
||||
@Import({ ServiceInvocationCounter.class, StubFooDao.class })
|
||||
@Import({ServiceInvocationCounter.class, StubFooDao.class})
|
||||
@EnableAspectJAutoProxy(exposeProxy = true)
|
||||
static class ConfigWithExposedProxy {
|
||||
|
||||
|
|
|
@ -22,9 +22,13 @@ import org.aspectj.lang.annotation.Aspect;
|
|||
import org.aspectj.lang.annotation.Before;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
|
||||
import org.springframework.beans.testfixture.beans.IOther;
|
||||
import org.springframework.beans.testfixture.beans.ITestBean;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
@ -32,10 +36,13 @@ import org.springframework.context.annotation.Bean;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.context.annotation.Proxyable;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.context.annotation.ProxyType.INTERFACES;
|
||||
import static org.springframework.context.annotation.ProxyType.TARGET_CLASS;
|
||||
|
||||
/**
|
||||
* System tests covering use of AspectJ {@link Aspect}s in conjunction with {@link Configuration} classes.
|
||||
|
@ -62,18 +69,40 @@ class ConfigurationClassAspectIntegrationTests {
|
|||
assertAdviceWasApplied(ConfigurationWithAspect.class);
|
||||
}
|
||||
|
||||
private void assertAdviceWasApplied(Class<?> configClass) {
|
||||
@Test
|
||||
void configurationIncludesAspectAndProxyable() {
|
||||
assertAdviceWasApplied(ConfigurationWithAspectAndProxyable.class, TestBean.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void configurationIncludesAspectAndProxyableInterfaces() {
|
||||
assertAdviceWasApplied(ConfigurationWithAspectAndProxyableInterfaces.class, TestBean.class, Comparable.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void configurationIncludesAspectAndProxyableTargetClass() {
|
||||
assertAdviceWasApplied(ConfigurationWithAspectAndProxyableTargetClass.class);
|
||||
}
|
||||
|
||||
private void assertAdviceWasApplied(Class<?> configClass, Class<?>... notImplemented) {
|
||||
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
|
||||
new XmlBeanDefinitionReader(factory).loadBeanDefinitions(
|
||||
new ClassPathResource("aspectj-autoproxy-config.xml", ConfigurationClassAspectIntegrationTests.class));
|
||||
GenericApplicationContext ctx = new GenericApplicationContext(factory);
|
||||
ctx.addBeanFactoryPostProcessor(new ConfigurationClassPostProcessor());
|
||||
ctx.registerBeanDefinition("config", new RootBeanDefinition(configClass));
|
||||
ctx.registerBeanDefinition("config",
|
||||
new RootBeanDefinition(configClass, AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR, false));
|
||||
ctx.refresh();
|
||||
|
||||
TestBean testBean = ctx.getBean("testBean", TestBean.class);
|
||||
ITestBean testBean = ctx.getBean("testBean", ITestBean.class);
|
||||
if (notImplemented.length > 0) {
|
||||
assertThat(testBean).isNotInstanceOfAny(notImplemented);
|
||||
}
|
||||
else {
|
||||
assertThat(testBean).isInstanceOf(TestBean.class);
|
||||
}
|
||||
assertThat(testBean.getName()).isEqualTo("name");
|
||||
testBean.absquatulate();
|
||||
((IOther) testBean).absquatulate();
|
||||
assertThat(testBean.getName()).isEqualTo("advisedName");
|
||||
ctx.close();
|
||||
}
|
||||
|
@ -120,6 +149,58 @@ class ConfigurationClassAspectIntegrationTests {
|
|||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class ConfigurationWithAspectAndProxyable {
|
||||
|
||||
@Bean
|
||||
@Proxyable(INTERFACES)
|
||||
public TestBean testBean() {
|
||||
return new TestBean("name");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NameChangingAspect nameChangingAspect() {
|
||||
return new NameChangingAspect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration()
|
||||
static class ConfigurationWithAspectAndProxyableInterfaces {
|
||||
|
||||
@Bean
|
||||
@Proxyable(interfaces = {ITestBean.class, IOther.class})
|
||||
public TestBean testBean() {
|
||||
return new TestBean("name");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NameChangingAspect nameChangingAspect() {
|
||||
return new NameChangingAspect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class ConfigurationWithAspectAndProxyableTargetClass {
|
||||
|
||||
public ConfigurationWithAspectAndProxyableTargetClass(AbstractAutoProxyCreator autoProxyCreator) {
|
||||
autoProxyCreator.setProxyTargetClass(false);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Proxyable(TARGET_CLASS)
|
||||
public TestBean testBean() {
|
||||
return new TestBean("name");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NameChangingAspect nameChangingAspect() {
|
||||
return new NameChangingAspect();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Aspect
|
||||
static class NameChangingAspect {
|
||||
|
||||
|
|
|
@ -355,6 +355,7 @@ class DefaultLifecycleProcessorTests {
|
|||
TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forShutdownTests(5, 0, stoppedBeans);
|
||||
TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forShutdownTests(-3, 0, stoppedBeans);
|
||||
smartBean2.setAutoStartup(false);
|
||||
smartBean2.setPauseable(false);
|
||||
context.getBeanFactory().registerSingleton("smartBean1", smartBean1);
|
||||
context.getBeanFactory().registerSingleton("smartBean2", smartBean2);
|
||||
|
||||
|
@ -375,11 +376,23 @@ class DefaultLifecycleProcessorTests {
|
|||
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1);
|
||||
assertThat(smartBean1.isRunning()).isTrue();
|
||||
assertThat(smartBean2.isRunning()).isFalse();
|
||||
context.pause();
|
||||
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1);
|
||||
assertThat(smartBean1.isRunning()).isFalse();
|
||||
assertThat(smartBean2.isRunning()).isFalse();
|
||||
context.restart();
|
||||
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1);
|
||||
assertThat(smartBean1.isRunning()).isTrue();
|
||||
assertThat(smartBean2.isRunning()).isFalse();
|
||||
context.start();
|
||||
assertThat(smartBean1.isRunning()).isTrue();
|
||||
assertThat(smartBean2.isRunning()).isTrue();
|
||||
context.pause();
|
||||
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean1);
|
||||
assertThat(smartBean1.isRunning()).isFalse();
|
||||
assertThat(smartBean2.isRunning()).isTrue();
|
||||
context.close();
|
||||
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean2);
|
||||
assertThat(stoppedBeans).containsExactly(smartBean1, smartBean1, smartBean1, smartBean1, smartBean2);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -740,6 +753,8 @@ class DefaultLifecycleProcessorTests {
|
|||
|
||||
private volatile boolean autoStartup = true;
|
||||
|
||||
private volatile boolean pauseable = true;
|
||||
|
||||
static TestSmartLifecycleBean forStartupTests(int phase, CopyOnWriteArrayList<Lifecycle> startedBeans) {
|
||||
return new TestSmartLifecycleBean(phase, 0, startedBeans, null);
|
||||
}
|
||||
|
@ -769,6 +784,15 @@ class DefaultLifecycleProcessorTests {
|
|||
this.autoStartup = autoStartup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseable() {
|
||||
return this.pauseable;
|
||||
}
|
||||
|
||||
public void setPauseable(boolean pauseable) {
|
||||
this.pauseable = pauseable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(final Runnable callback) {
|
||||
// calling stop() before the delay to preserve
|
||||
|
|
|
@ -189,6 +189,26 @@ class ReactiveRetryInterceptorTests {
|
|||
assertThat(target.counter.get()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void adaptReactiveResultWithZeroAttempts() {
|
||||
// Test minimal retry configuration: maxAttempts=1, delay=0, jitter=0, multiplier=1.0, maxDelay=0
|
||||
MinimalRetryBean target = new MinimalRetryBean();
|
||||
ProxyFactory pf = new ProxyFactory();
|
||||
pf.setTarget(target);
|
||||
pf.addAdvice(new SimpleRetryInterceptor(
|
||||
new MethodRetrySpec((m, t) -> true, 0, Duration.ZERO, Duration.ZERO, 1.0, Duration.ZERO)));
|
||||
MinimalRetryBean proxy = (MinimalRetryBean) pf.getProxy();
|
||||
|
||||
// Should execute only 1 time, because maxAttempts=0 means initial call only
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> proxy.retryOperation().block())
|
||||
.satisfies(isRetryExhaustedException())
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("1");
|
||||
assertThat(target.counter.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void adaptReactiveResultWithZeroDelayAndJitter() {
|
||||
// Test case where delay=0 and jitter>0
|
||||
|
|
|
@ -17,16 +17,22 @@
|
|||
package org.springframework.resilience;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.time.Duration;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.aop.framework.ProxyConfig;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
|
||||
import org.springframework.aop.interceptor.SimpleTraceInterceptor;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
@ -56,7 +62,30 @@ class RetryInterceptorTests {
|
|||
pf.setTarget(target);
|
||||
pf.addAdvice(new SimpleRetryInterceptor(
|
||||
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
|
||||
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy();
|
||||
pf.addAdvice(new SimpleTraceInterceptor());
|
||||
PlainInterface proxy = (PlainInterface) pf.getProxy();
|
||||
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withSimpleInterceptorAndNoTarget() {
|
||||
NonAnnotatedBean target = new NonAnnotatedBean();
|
||||
ProxyFactory pf = new ProxyFactory();
|
||||
pf.addAdvice(new SimpleRetryInterceptor(
|
||||
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
|
||||
pf.addAdvice(new SimpleTraceInterceptor());
|
||||
pf.addAdvice((MethodInterceptor) invocation -> {
|
||||
try {
|
||||
return invocation.getMethod().invoke(target, invocation.getArguments());
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
throw ex.getTargetException();
|
||||
}
|
||||
});
|
||||
pf.addInterface(PlainInterface.class);
|
||||
PlainInterface proxy = (PlainInterface) pf.getProxy();
|
||||
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
|
@ -76,6 +105,78 @@ class RetryInterceptorTests {
|
|||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethodWithInterface() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
|
||||
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethodWithInterfaceAndDefaultTargetClass() {
|
||||
ProxyConfig defaultProxyConfig = new ProxyConfig();
|
||||
defaultProxyConfig.setProxyTargetClass(true);
|
||||
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig);
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
|
||||
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethodWithInterfaceAndPreserveTargetClass() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class);
|
||||
bd.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
|
||||
bf.registerBeanDefinition("bean", bd);
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
|
||||
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethodWithInterfaceAndExposeInterfaces() {
|
||||
ProxyConfig defaultProxyConfig = new ProxyConfig();
|
||||
defaultProxyConfig.setProxyTargetClass(true);
|
||||
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig);
|
||||
RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class);
|
||||
bd.setAttribute(AutoProxyUtils.EXPOSED_INTERFACES_ATTRIBUTE, AutoProxyUtils.ALL_INTERFACES_ATTRIBUTE_VALUE);
|
||||
bf.registerBeanDefinition("bean", bd);
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
|
||||
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPostProcessorForClass() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
|
@ -119,6 +220,31 @@ class RetryInterceptorTests {
|
|||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPostProcessorForClassWithZeroAttempts() {
|
||||
Properties props = new Properties();
|
||||
props.setProperty("delay", "10");
|
||||
props.setProperty("jitter", "5");
|
||||
props.setProperty("multiplier", "2.0");
|
||||
props.setProperty("maxDelay", "40");
|
||||
props.setProperty("limitedAttempts", "0");
|
||||
|
||||
GenericApplicationContext ctx = new GenericApplicationContext();
|
||||
ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("props", props));
|
||||
ctx.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBeanWithStrings.class));
|
||||
ctx.registerBeanDefinition("bpp", new RootBeanDefinition(RetryAnnotationBeanPostProcessor.class));
|
||||
ctx.refresh();
|
||||
AnnotatedClassBeanWithStrings proxy = ctx.getBean(AnnotatedClassBeanWithStrings.class);
|
||||
AnnotatedClassBeanWithStrings target = (AnnotatedClassBeanWithStrings) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3");
|
||||
assertThat(target.counter).isEqualTo(3);
|
||||
assertThatIOException().isThrownBy(proxy::otherOperation);
|
||||
assertThat(target.counter).isEqualTo(4);
|
||||
assertThatIOException().isThrownBy(proxy::overrideOperation);
|
||||
assertThat(target.counter).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withEnableAnnotation() throws Exception {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
|
@ -137,7 +263,7 @@ class RetryInterceptorTests {
|
|||
}
|
||||
|
||||
|
||||
static class NonAnnotatedBean {
|
||||
static class NonAnnotatedBean implements PlainInterface {
|
||||
|
||||
int counter = 0;
|
||||
|
||||
|
@ -148,6 +274,12 @@ class RetryInterceptorTests {
|
|||
}
|
||||
|
||||
|
||||
public interface PlainInterface {
|
||||
|
||||
void retryOperation() throws IOException;
|
||||
}
|
||||
|
||||
|
||||
static class AnnotatedMethodBean {
|
||||
|
||||
int counter = 0;
|
||||
|
@ -160,6 +292,26 @@ class RetryInterceptorTests {
|
|||
}
|
||||
|
||||
|
||||
static class AnnotatedMethodBeanWithInterface implements AnnotatedInterface {
|
||||
|
||||
int counter = 0;
|
||||
|
||||
@Retryable(maxAttempts = 5, delay = 10)
|
||||
@Override
|
||||
public void retryOperation() throws IOException {
|
||||
counter++;
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface AnnotatedInterface {
|
||||
|
||||
@Retryable(maxAttempts = 5, delay = 10)
|
||||
void retryOperation() throws IOException;
|
||||
}
|
||||
|
||||
|
||||
@Retryable(delay = 10, jitter = 5, multiplier = 2.0, maxDelay = 40,
|
||||
includes = IOException.class, excludes = AccessDeniedException.class,
|
||||
predicate = CustomPredicate.class)
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
example.scannable.AutowiredQualifierFooService=example.scannable.FooService
|
||||
example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component
|
||||
example.scannable.NamedComponent=org.springframework.stereotype.Component
|
||||
example.scannable.FooService=example.scannable.FooService
|
||||
example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService
|
||||
example.scannable.ScopedProxyTestBean=example.scannable.FooService
|
||||
example.scannable.StubFooDao=org.springframework.stereotype.Component
|
||||
example.scannable.NamedComponent=org.springframework.stereotype.Component
|
||||
example.scannable.NamedStubDao=org.springframework.stereotype.Component
|
||||
example.scannable.OtherFooService=org.springframework.stereotype.Component,example.scannable.FooService
|
||||
example.scannable.ScopedProxyTestBean=example.scannable.FooService
|
||||
example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component
|
||||
example.scannable.StubFooDao=org.springframework.stereotype.Component
|
||||
example.scannable.sub.BarComponent=org.springframework.stereotype.Component
|
||||
example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean
|
||||
example.scannable.JakartaNamedComponent=jakarta.inject.Named
|
||||
|
|
|
@ -14,7 +14,7 @@ multiRelease {
|
|||
releaseVersions 21, 24
|
||||
}
|
||||
|
||||
def javapoetVersion = "1.13.0"
|
||||
def javapoetVersion = "0.7.0"
|
||||
def objenesisVersion = "3.4"
|
||||
|
||||
configurations {
|
||||
|
@ -30,12 +30,12 @@ tasks.register('javapoetRepackJar', ShadowJar) {
|
|||
archiveBaseName = 'spring-javapoet-repack'
|
||||
archiveVersion = javapoetVersion
|
||||
configurations = [project.configurations.javapoet]
|
||||
relocate('com.squareup.javapoet', 'org.springframework.javapoet')
|
||||
relocate('com.palantir.javapoet', 'org.springframework.javapoet')
|
||||
}
|
||||
|
||||
tasks.register('javapoetSource', ShadowSource) {
|
||||
configurations = [project.configurations.javapoet]
|
||||
relocate('com.squareup.javapoet', 'org.springframework.javapoet')
|
||||
relocate('com.palantir.javapoet', 'org.springframework.javapoet')
|
||||
outputDirectory = file("build/shadow-source/javapoet")
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ tasks.register('objenesisSourceJar', Jar) {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
javapoet("com.squareup:javapoet:${javapoetVersion}@jar")
|
||||
javapoet("com.palantir.javapoet:javapoet:${javapoetVersion}@jar")
|
||||
objenesis("org.objenesis:objenesis:${objenesisVersion}@jar")
|
||||
api(files(javapoetRepackJar))
|
||||
api(files(objenesisRepackJar))
|
||||
|
@ -96,7 +96,7 @@ dependencies {
|
|||
testImplementation("com.fasterxml.jackson.core:jackson-databind")
|
||||
testImplementation("com.fasterxml.woodstox:woodstox-core")
|
||||
testImplementation("com.google.code.findbugs:jsr305")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver3")
|
||||
testImplementation("io.projectreactor:reactor-test")
|
||||
testImplementation("io.projectreactor.tools:blockhound")
|
||||
testImplementation("jakarta.annotation:jakarta.annotation-api")
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
|
|||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.CodeBlock;
|
||||
import org.springframework.javapoet.MethodSpec;
|
||||
import org.springframework.javapoet.ParameterSpec;
|
||||
import org.springframework.javapoet.TypeName;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
@ -51,7 +52,7 @@ public class DefaultMethodReference implements MethodReference {
|
|||
|
||||
@Override
|
||||
public CodeBlock toCodeBlock() {
|
||||
String methodName = this.method.name;
|
||||
String methodName = this.method.name();
|
||||
if (isStatic()) {
|
||||
Assert.state(this.declaringClass != null, "Static method reference must define a declaring class");
|
||||
return CodeBlock.of("$T::$L", this.declaringClass, methodName);
|
||||
|
@ -65,7 +66,7 @@ public class DefaultMethodReference implements MethodReference {
|
|||
public CodeBlock toInvokeCodeBlock(ArgumentCodeGenerator argumentCodeGenerator,
|
||||
@Nullable ClassName targetClassName) {
|
||||
|
||||
String methodName = this.method.name;
|
||||
String methodName = this.method.name();
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
if (isStatic()) {
|
||||
Assert.state(this.declaringClass != null, "Static method reference must define a declaring class");
|
||||
|
@ -96,8 +97,8 @@ public class DefaultMethodReference implements MethodReference {
|
|||
*/
|
||||
protected void addArguments(CodeBlock.Builder code, ArgumentCodeGenerator argumentCodeGenerator) {
|
||||
List<CodeBlock> arguments = new ArrayList<>();
|
||||
TypeName[] argumentTypes = this.method.parameters.stream()
|
||||
.map(parameter -> parameter.type).toArray(TypeName[]::new);
|
||||
TypeName[] argumentTypes = this.method.parameters().stream()
|
||||
.map(ParameterSpec::type).toArray(TypeName[]::new);
|
||||
for (int i = 0; i < argumentTypes.length; i++) {
|
||||
TypeName argumentType = argumentTypes[i];
|
||||
CodeBlock argumentCode = argumentCodeGenerator.generateCode(argumentType);
|
||||
|
@ -115,12 +116,12 @@ public class DefaultMethodReference implements MethodReference {
|
|||
}
|
||||
|
||||
private boolean isStatic() {
|
||||
return this.method.modifiers.contains(Modifier.STATIC);
|
||||
return this.method.modifiers().contains(Modifier.STATIC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String methodName = this.method.name;
|
||||
String methodName = this.method.name();
|
||||
if (isStatic()) {
|
||||
return this.declaringClass + "::" + methodName;
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ public interface GeneratedFiles {
|
|||
* @param javaFile the java file to add
|
||||
*/
|
||||
default void addSourceFile(JavaFile javaFile) {
|
||||
validatePackage(javaFile.packageName, javaFile.typeSpec.name);
|
||||
String className = javaFile.packageName + "." + javaFile.typeSpec.name;
|
||||
validatePackage(javaFile.packageName(), javaFile.typeSpec().name());
|
||||
String className = javaFile.packageName() + "." + javaFile.typeSpec().name();
|
||||
addSourceFile(className, javaFile::writeTo);
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ public final class GeneratedMethod {
|
|||
MethodSpec.Builder builder = MethodSpec.methodBuilder(this.name);
|
||||
method.accept(builder);
|
||||
this.methodSpec = builder.build();
|
||||
Assert.state(this.name.equals(this.methodSpec.name),
|
||||
Assert.state(this.name.equals(this.methodSpec.name()),
|
||||
"'method' consumer must not change the generated method name");
|
||||
}
|
||||
|
||||
|
|
|
@ -202,7 +202,7 @@ public class AnnotatedMethod {
|
|||
}
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
if (paramTypes[i] !=
|
||||
ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).resolve()) {
|
||||
ResolvableType.forMethodParameter(candidate, i, this.method.getDeclaringClass()).toClass()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -374,7 +374,7 @@ abstract class AnnotationsScanner {
|
|||
}
|
||||
for (int i = 0; i < rootParameterTypes.length; i++) {
|
||||
Class<?> resolvedParameterType = ResolvableType.forMethodParameter(
|
||||
candidateMethod, i, sourceDeclaringClass).resolve();
|
||||
candidateMethod, i, sourceDeclaringClass).toClass();
|
||||
if (rootParameterTypes[i] != resolvedParameterType) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ import java.util.function.Predicate;
|
|||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
|
@ -483,7 +482,7 @@ final class TypeMappedAnnotations implements MergedAnnotations {
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Aggregate> finish(@Nullable List<Aggregate> processResult) {
|
||||
public List<Aggregate> finish(@Nullable List<Aggregate> processResult) {
|
||||
return this.aggregates;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -361,12 +361,22 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
|
|||
* @throws IOException if thrown from URLConnection methods
|
||||
*/
|
||||
protected void customizeConnection(URLConnection con) throws IOException {
|
||||
ResourceUtils.useCachesIfNecessary(con);
|
||||
useCachesIfNecessary(con);
|
||||
if (con instanceof HttpURLConnection httpCon) {
|
||||
customizeConnection(httpCon);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply {@link URLConnection#setUseCaches useCaches} if necessary.
|
||||
* @param con the URLConnection to customize
|
||||
* @since 6.2.10
|
||||
* @see ResourceUtils#useCachesIfNecessary(URLConnection)
|
||||
*/
|
||||
void useCachesIfNecessary(URLConnection con) {
|
||||
ResourceUtils.useCachesIfNecessary(con);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize the given {@link HttpURLConnection} before fetching the resource.
|
||||
* <p>Can be overridden in subclasses for configuring request headers and timeouts.
|
||||
|
|
|
@ -109,7 +109,9 @@ public class FileUrlResource extends UrlResource implements WritableResource {
|
|||
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) throws MalformedURLException {
|
||||
return new FileUrlResource(createRelativeURL(relativePath));
|
||||
FileUrlResource resource = new FileUrlResource(createRelativeURL(relativePath));
|
||||
resource.useCaches = this.useCaches;
|
||||
return resource;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue