mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5356a1b1ac | |||
| f7307c9e07 | |||
| 098c4b1dd7 | |||
| 65dbfd09b4 | |||
| 203fa75196 | |||
| 9a56a8877f | |||
| e79a9a5bff | |||
| 2c3c3831c1 | |||
| c38e9896c7 | |||
| 3e0849a566 | |||
| f3a605b92c | |||
| 6fdff201e9 | |||
| c42778a05b | |||
| f140df881d | |||
| 12cf654c98 | |||
| 089e4e69f1 | |||
| 77755ff2fa | |||
| d0aa7ad524 | |||
| 072fc80cdb | |||
| 6f32ff489a | |||
| 24c8dfea1f | |||
| cdfe5816c8 | |||
| 0ff200b2f1 | |||
| 261dac87cc | |||
| fce2f49e46 | |||
| eca2b9657e | |||
| e48f37d956 | |||
| ddc397dd05 | |||
| 6b7f0bd4b6 | |||
| c97a895f09 | |||
| 3b13f2ed38 | |||
| a0eebca0cf | |||
| c6c64e6fe7 | |||
| 0ca393c0dc | |||
| 6681394886 | |||
| 172987c874 | |||
| 47a5ebfde6 | |||
| 43a113f067 | |||
| dc250e1cc1 | |||
| fe74fcfded | |||
| 2451bd62b0 | |||
| 624d6dd167 | |||
| 0ea96b4806 | |||
| c28a0d5627 | |||
| 61d045ce52 | |||
| c0bef2c693 | |||
| c3a0eaa95e | |||
| e12d1259d1 | |||
| 404c4d9d92 | |||
| 7785f94c4c | |||
| 4f6f2c0d41 | |||
| e6da2a86fc | |||
| 301087e510 | |||
| 6c054f88ea | |||
| f58c7d80cc | |||
| e5be10d53d | |||
| 524da905db | |||
| 4323c60513 | |||
| f6b608eecb | |||
| 4da1511ed3 | |||
| 7102c33661 | |||
| 43409b00d0 | |||
| d55abc6cf9 | |||
| 31806f3a6b | |||
| 8a84241c1e | |||
| 624be6d4e6 | |||
| b08883b65c | |||
| 542ba3517f | |||
| 8c6a7799be | |||
| 557dbba585 | |||
| e9de426eb5 | |||
| cda577d1aa | |||
| 8feb842df5 | |||
| ea2931f24a | |||
| 26d1c38d84 | |||
| 345daaabbc | |||
| 6c08d93992 | |||
| 6d7cd9c7dc | |||
| 73eb6f0660 | |||
| 31f298b929 | |||
| c01aab5850 | |||
| 61ef5a8930 | |||
| 3b53ee7038 |
@@ -1,44 +0,0 @@
|
||||
= Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project, and in the interest of fostering an open
|
||||
and welcoming community, we pledge to respect all people who contribute through reporting
|
||||
issues, posting feature requests, updating documentation, submitting pull requests or
|
||||
patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project a harassment-free experience for
|
||||
everyone, regardless of level of experience, gender, gender identity and expression,
|
||||
sexual orientation, disability, personal appearance, body size, race, ethnicity, age,
|
||||
religion, or nationality.
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery
|
||||
* Personal attacks
|
||||
* Trolling or insulting/derogatory comments
|
||||
* Public or private harassment
|
||||
* Publishing other's private information, such as physical or electronic addresses,
|
||||
without explicit permission
|
||||
* Other unethical or unprofessional conduct
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments,
|
||||
commits, code, wiki edits, issues, and other contributions that are not aligned to this
|
||||
Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors
|
||||
that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
By adopting this Code of Conduct, project maintainers commit themselves to fairly and
|
||||
consistently applying these principles to every aspect of managing this project. Project
|
||||
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
|
||||
from the project team.
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an
|
||||
individual is representing the project or its community.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
|
||||
contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will
|
||||
be reviewed and investigated and will result in a response that is deemed necessary and
|
||||
appropriate to the circumstances. Maintainers are obligated to maintain confidentiality
|
||||
with regard to the reporter of an incident.
|
||||
|
||||
This Code of Conduct is adapted from the
|
||||
https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at
|
||||
https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]
|
||||
+1
-1
@@ -18,7 +18,7 @@ First off, thank you for taking the time to contribute! :+1: :tada:
|
||||
|
||||
This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc).
|
||||
By participating you are expected to uphold this code.
|
||||
Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
Please report unacceptable behavior to spring-code-of-conduct@spring.io.
|
||||
|
||||
### How to Contribute
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Spring provides everything required beyond the Java programming language for cre
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc). By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc). By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to spring-code-of-conduct@spring.io.
|
||||
|
||||
## Access to Binaries
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
org.gradle.caching=true
|
||||
javaFormatVersion=0.0.41
|
||||
javaFormatVersion=0.0.42
|
||||
|
||||
@@ -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.16.0");
|
||||
checkstyle.setToolVersion("10.17.0");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
@@ -64,7 +64,7 @@ public class CheckstyleConventions {
|
||||
NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class);
|
||||
noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines"));
|
||||
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**",
|
||||
"**/.classpath", "**/.project", "**/.gradle/**");
|
||||
"**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**");
|
||||
List<String> buildFolders = List.of("bin", "build", "out");
|
||||
project.allprojects(subproject -> {
|
||||
Path rootPath = project.getRootDir().toPath();
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ anchors:
|
||||
gradle-enterprise-task-params: &gradle-enterprise-task-params
|
||||
DEVELOCITY_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
sonatype-task-params: &sonatype-task-params
|
||||
SONATYPE_USERNAME: ((sonatype-username))
|
||||
SONATYPE_PASSWORD: ((sonatype-password))
|
||||
SONATYPE_USERNAME: ((s01-user-token))
|
||||
SONATYPE_PASSWORD: ((s01-user-token-password))
|
||||
SONATYPE_URL: ((sonatype-url))
|
||||
SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile))
|
||||
artifactory-task-params: &artifactory-task-params
|
||||
|
||||
@@ -36,4 +36,4 @@ runtime:
|
||||
failure_level: warn
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
|
||||
|
||||
@@ -221,8 +221,8 @@ NOTE: `ThreadLocal` instances come with serious issues (potentially resulting in
|
||||
incorrectly using them in multi-threaded and multi-classloader environments. You
|
||||
should always consider wrapping a `ThreadLocal` in some other class and never directly use
|
||||
the `ThreadLocal` itself (except in the wrapper class). Also, you should
|
||||
always remember to correctly set and unset (where the latter simply involves a call to
|
||||
`ThreadLocal.set(null)`) the resource local to the thread. Unsetting should be done in
|
||||
always remember to correctly set and unset (where the latter involves a call to
|
||||
`ThreadLocal.remove()`) the resource local to the thread. Unsetting should be done in
|
||||
any case, since not unsetting it might result in problematic behavior. Spring's
|
||||
`ThreadLocal` support does this for you and should always be considered in favor of using
|
||||
`ThreadLocal` instances without other proper handling code.
|
||||
|
||||
@@ -326,6 +326,19 @@ However, this is not a best practice and flagging the preferred constructor with
|
||||
|
||||
In case you are working on a code base that you cannot modify, you can set the {spring-framework-api}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used.
|
||||
|
||||
[[aot.bestpractices.comlext-data-structure]]
|
||||
=== Avoid Complex Data Structure for Constructor Parameters and Properties
|
||||
|
||||
When crafting a `RootBeanDefinition` programmatically, you are not constrained in terms of types that you can use.
|
||||
For instance, you may have a custom `record` with several properties that your bean takes as a constructor argument.
|
||||
|
||||
While this works fine with the regular runtime, AOT does not know how to generate the code of your custom data structure.
|
||||
A good rule of thumb is to keep in mind that bean definitions are an abstraction on top of several models.
|
||||
Rather than using such structure, decomposing to simple types or referring to a bean that is built as such is recommended.
|
||||
|
||||
As a last resort, you can implement your own `org.springframework.aot.generate.ValueCodeGenerator$Delegate`.
|
||||
To use it, register its fully qualified name in `META-INF/spring/aot.factories` using the `Delegate` as the key.
|
||||
|
||||
[[aot.bestpractices.custom-arguments]]
|
||||
=== Avoid Creating Bean with Custom Arguments
|
||||
|
||||
|
||||
+5
-7
@@ -153,17 +153,15 @@ Letting qualifier values select against target bean names, within the type-match
|
||||
candidates, does not require a `@Qualifier` annotation at the injection point.
|
||||
If there is no other resolution indicator (such as a qualifier or a primary marker),
|
||||
for a non-unique dependency situation, Spring matches the injection point name
|
||||
(that is, the field name or parameter name) against the target bean names and chooses the
|
||||
same-named candidate, if any.
|
||||
(that is, the field name or parameter name) against the target bean names and chooses
|
||||
the same-named candidate, if any (either by bean name or by associated alias).
|
||||
|
||||
Since version 6.1, this requires the `-parameters` Java compiler flag to be present.
|
||||
====
|
||||
|
||||
That said, if you intend to express annotation-driven injection by name, do not
|
||||
primarily use `@Autowired`, even if it is capable of selecting by bean name among
|
||||
type-matching candidates. Instead, use the JSR-250 `@Resource` annotation, which is
|
||||
semantically defined to identify a specific target component by its unique name, with
|
||||
the declared type being irrelevant for the matching process. `@Autowired` has rather
|
||||
As an alternative for injection by name, consider the JSR-250 `@Resource` annotation
|
||||
which is semantically defined to identify a specific target component by its unique name,
|
||||
with the declared type being irrelevant for the matching process. `@Autowired` has rather
|
||||
different semantics: After selecting candidate beans by type, the specified `String`
|
||||
qualifier value is considered within those type-selected candidates only (for example,
|
||||
matching an `account` qualifier against beans marked with the same qualifier label).
|
||||
|
||||
@@ -75,6 +75,31 @@ lead to concurrent access exceptions, inconsistent state in the bean container,
|
||||
|
||||
|
||||
|
||||
[[beans-definition-overriding]]
|
||||
== Overriding Beans
|
||||
|
||||
Bean overriding is happening when a bean is registered using an identifier that is
|
||||
already allocated. While bean overriding is possible, it makes the configuration harder
|
||||
to read and this feature will be deprecated in a future release.
|
||||
|
||||
To disable bean overriding altogether, you can set the `allowBeanDefinitionOverriding`
|
||||
flag to `false` on the `ApplicationContext` before it is refreshed. In such setup, an
|
||||
exception is thrown if bean overriding is used.
|
||||
|
||||
By default, the container logs every bean overriding at `INFO` level so that you can
|
||||
adapt your configuration accordingly. While not recommended, you can silence those logs
|
||||
by setting the `allowBeanDefinitionOverriding` flag to `true`.
|
||||
|
||||
.Java-configuration
|
||||
****
|
||||
If you use Java Configuration, a corresponding `@Bean` method always silently overrides
|
||||
a scanned bean class with the same component name as long as the return type of the
|
||||
`@Bean` method matches that bean class. This simply means that the container will call
|
||||
the `@Bean` factory method in favor of any pre-declared constructor on the bean class.
|
||||
****
|
||||
|
||||
|
||||
|
||||
[[beans-beanname]]
|
||||
== Naming Beans
|
||||
|
||||
|
||||
@@ -439,7 +439,7 @@ dataSource.url=jdbc:mysql:mydb
|
||||
----
|
||||
|
||||
This example file can be used with a container definition that contains a bean called
|
||||
`dataSource` that has `driver` and `url` properties.
|
||||
`dataSource` that has `driverClassName` and `url` properties.
|
||||
|
||||
Compound property names are also supported, as long as every component of the path
|
||||
except the final property being overridden is already non-null (presumably initialized
|
||||
|
||||
@@ -299,7 +299,7 @@ Java::
|
||||
public class AppConfig {
|
||||
|
||||
@Bean
|
||||
public MethodValidationPostProcessor validationPostProcessor() {
|
||||
public static MethodValidationPostProcessor validationPostProcessor() {
|
||||
return new MethodValidationPostProcessor();
|
||||
}
|
||||
}
|
||||
@@ -341,7 +341,7 @@ xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] sections.
|
||||
=== Method Validation Exceptions
|
||||
|
||||
By default, `jakarta.validation.ConstraintViolationException` is raised with the set of
|
||||
``ConstraintViolation``s returned by `jakarata.validation.Validator`. As an alternative,
|
||||
``ConstraintViolation``s returned by `jakarta.validation.Validator`. As an alternative,
|
||||
you can have `MethodValidationException` raised instead with ``ConstraintViolation``s
|
||||
adapted to `MessageSourceResolvable` errors. To enable set the following flag:
|
||||
|
||||
@@ -357,7 +357,7 @@ Java::
|
||||
public class AppConfig {
|
||||
|
||||
@Bean
|
||||
public MethodValidationPostProcessor validationPostProcessor() {
|
||||
public static MethodValidationPostProcessor validationPostProcessor() {
|
||||
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
|
||||
processor.setAdaptConstraintViolations(true);
|
||||
return processor;
|
||||
|
||||
@@ -112,7 +112,7 @@ Java::
|
||||
this.actorMappingQuery = new ActorMappingQuery(dataSource);
|
||||
}
|
||||
|
||||
public Customer getCustomer(Long id) {
|
||||
public Actor getActor(Long id) {
|
||||
return actorMappingQuery.findObject(id);
|
||||
}
|
||||
----
|
||||
@@ -123,11 +123,11 @@ Kotlin::
|
||||
----
|
||||
private val actorMappingQuery = ActorMappingQuery(dataSource)
|
||||
|
||||
fun getCustomer(id: Long) = actorMappingQuery.findObject(id)
|
||||
fun getActor(id: Long) = actorMappingQuery.findObject(id)
|
||||
----
|
||||
======
|
||||
|
||||
The method in the preceding example retrieves the customer with the `id` that is passed in as the
|
||||
The method in the preceding example retrieves the actor with the `id` that is passed in as the
|
||||
only parameter. Since we want only one object to be returned, we call the `findObject` convenience
|
||||
method with the `id` as the parameter. If we had instead a query that returned a
|
||||
list of objects and took additional parameters, we would use one of the `execute`
|
||||
|
||||
@@ -29,8 +29,6 @@ Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clem
|
||||
Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane
|
||||
Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch
|
||||
|
||||
Copyright © 2002 - 2024 VMware, Inc. All Rights Reserved.
|
||||
|
||||
Copies of this document may be made for your own use and for distribution to others,
|
||||
provided that you do not charge any fee for such copies and further provided that each
|
||||
copy contains this Copyright Notice, whether distributed in print or electronically.
|
||||
copy contains the Copyright Notice, whether distributed in print or electronically.
|
||||
|
||||
@@ -58,7 +58,7 @@ your `@Configuration` classes, as the following example shows:
|
||||
By default, the infrastructure looks for a bean named `jmsListenerContainerFactory`
|
||||
as the source for the factory to use to create message listener containers. In this
|
||||
case (and ignoring the JMS infrastructure setup), you can invoke the `processOrder`
|
||||
method with a core poll size of three threads and a maximum pool size of ten threads.
|
||||
method with a core pool size of three threads and a maximum pool size of ten threads.
|
||||
|
||||
You can customize the listener container factory to use for each annotation or you can
|
||||
configure an explicit default by implementing the `JmsListenerConfigurer` interface.
|
||||
|
||||
@@ -97,8 +97,8 @@ Java::
|
||||
Mono<Person> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatus::is4xxClientError, response -> ...)
|
||||
.onStatus(HttpStatus::is5xxServerError, response -> ...)
|
||||
.onStatus(HttpStatusCode::is4xxClientError, response -> ...)
|
||||
.onStatus(HttpStatusCode::is5xxServerError, response -> ...)
|
||||
.bodyToMono(Person.class);
|
||||
----
|
||||
|
||||
@@ -109,8 +109,8 @@ Kotlin::
|
||||
val result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatus::is4xxClientError) { ... }
|
||||
.onStatus(HttpStatus::is5xxServerError) { ... }
|
||||
.onStatus(HttpStatusCode::is4xxClientError) { ... }
|
||||
.onStatus(HttpStatusCode::is5xxServerError) { ... }
|
||||
.awaitBody<Person>()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -338,7 +338,7 @@ Kotlin::
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
|
||||
// ...
|
||||
configurer.defaultCodecs().maxInMemorySize(512 * 1024)
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -13,7 +13,7 @@ If configured with a task scheduler, the simple broker supports
|
||||
https://stomp.github.io/stomp-specification-1.2.html#Heart-beating[STOMP heartbeats].
|
||||
To configure a scheduler, you can declare your own `TaskScheduler` bean and set it through
|
||||
the `MessageBrokerRegistry`. Alternatively, you can use the one that is automatically
|
||||
declared in the built-in WebSocket configuration, however, you'll' need `@Lazy` to avoid
|
||||
declared in the built-in WebSocket configuration, however, you'll need `@Lazy` to avoid
|
||||
a cycle between the built-in WebSocket configuration and your
|
||||
`WebSocketMessageBrokerConfigurer`. For example:
|
||||
|
||||
|
||||
@@ -8,16 +8,16 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.15.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.6"))
|
||||
api(platform("io.netty:netty-bom:4.1.109.Final"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.7"))
|
||||
api(platform("io.netty:netty-bom:4.1.111.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.6"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.7"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.21"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.25.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.9"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.9"))
|
||||
api(platform("org.assertj:assertj-bom:3.26.0"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.10"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.10"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0"))
|
||||
api(platform("org.junit:junit-bom:5.10.2"))
|
||||
@@ -110,7 +110,7 @@ dependencies {
|
||||
api("org.aspectj:aspectjrt:1.9.22.1")
|
||||
api("org.aspectj:aspectjtools:1.9.22.1")
|
||||
api("org.aspectj:aspectjweaver:1.9.22.1")
|
||||
api("org.awaitility:awaitility:4.2.0")
|
||||
api("org.awaitility:awaitility:4.2.1")
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.4.0")
|
||||
@@ -130,7 +130,7 @@ dependencies {
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.2")
|
||||
api("org.javamoney:moneta:1.4.2")
|
||||
api("org.jruby:jruby:9.4.6.0")
|
||||
api("org.jruby:jruby:9.4.7.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
api("org.mozilla:rhino:1.7.14")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
@@ -139,7 +139,7 @@ dependencies {
|
||||
api("org.seleniumhq.selenium:htmlunit-driver:2.70.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:3.141.59")
|
||||
api("org.skyscreamer:jsonassert:1.5.1")
|
||||
api("org.slf4j:slf4j-api:2.0.12")
|
||||
api("org.slf4j:slf4j-api:2.0.13")
|
||||
api("org.testng:testng:7.9.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.55")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.1.8-SNAPSHOT
|
||||
version=6.1.10
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -29,7 +29,7 @@ publishing {
|
||||
developer {
|
||||
id = "jhoeller"
|
||||
name = "Juergen Hoeller"
|
||||
email = "jhoeller@pivotal.io"
|
||||
email = "juergen.hoeller@broadcom.com"
|
||||
}
|
||||
}
|
||||
issueManagement {
|
||||
|
||||
@@ -87,12 +87,12 @@ rootProject.ext {
|
||||
gradle.taskGraph.afterTask { Task task, TaskState state ->
|
||||
if (!resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) {
|
||||
def metadata = task.javaCompiler.get().metadata
|
||||
task.project.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)")
|
||||
task.project.develocity.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)")
|
||||
resolvedMainToolchain = true
|
||||
}
|
||||
if (testToolchainConfigured() && !resolvedTestToolchain && task instanceof Test && task.javaLauncher.isPresent()) {
|
||||
def metadata = task.javaLauncher.get().metadata
|
||||
task.project.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)")
|
||||
task.project.develocity.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)")
|
||||
resolvedTestToolchain = true
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ class EnableCachingIntegrationTests {
|
||||
// attempt was made to look up the AJ aspect. It's due to classpath issues
|
||||
// in integration-tests that it's not found.
|
||||
assertThatException().isThrownBy(ctx::refresh)
|
||||
.withMessageContaining("AspectJCachingConfiguration");
|
||||
.withMessageContaining("AspectJCachingConfiguration");
|
||||
}
|
||||
|
||||
|
||||
|
||||
Vendored
+1
-2
@@ -541,8 +541,7 @@ public class EnvironmentSystemIntegrationTests {
|
||||
{
|
||||
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
ctx.getEnvironment().setRequiredProperties("foo", "bar");
|
||||
assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(
|
||||
ctx::refresh);
|
||||
assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(ctx::refresh);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
+2
-3
@@ -97,9 +97,8 @@ class EnableTransactionManagementIntegrationTests {
|
||||
// this test is a bit fragile, but gets the job done, proving that an
|
||||
// attempt was made to look up the AJ aspect. It's due to classpath issues
|
||||
// in integration-tests that it's not found.
|
||||
assertThatException()
|
||||
.isThrownBy(ctx::refresh)
|
||||
.withMessageContaining("AspectJJtaTransactionManagementConfiguration");
|
||||
assertThatException().isThrownBy(ctx::refresh)
|
||||
.withMessageContaining("AspectJJtaTransactionManagementConfiguration");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+21
-1
@@ -18,6 +18,7 @@ package org.springframework.aop.aspectj;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Arrays;
|
||||
@@ -86,6 +87,8 @@ import org.springframework.util.StringUtils;
|
||||
public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware {
|
||||
|
||||
private static final String AJC_MAGIC = "ajc$";
|
||||
|
||||
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = Set.of(
|
||||
PointcutPrimitive.EXECUTION,
|
||||
PointcutPrimitive.ARGS,
|
||||
@@ -103,6 +106,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
@Nullable
|
||||
private Class<?> pointcutDeclarationScope;
|
||||
|
||||
private boolean aspectCompiledByAjc;
|
||||
|
||||
private String[] pointcutParameterNames = new String[0];
|
||||
|
||||
private Class<?>[] pointcutParameterTypes = new Class<?>[0];
|
||||
@@ -134,7 +139,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
* @param paramTypes the parameter types for the pointcut
|
||||
*/
|
||||
public AspectJExpressionPointcut(Class<?> declarationScope, String[] paramNames, Class<?>[] paramTypes) {
|
||||
this.pointcutDeclarationScope = declarationScope;
|
||||
setPointcutDeclarationScope(declarationScope);
|
||||
if (paramNames.length != paramTypes.length) {
|
||||
throw new IllegalStateException(
|
||||
"Number of pointcut parameter names must match number of pointcut parameter types");
|
||||
@@ -149,6 +154,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
*/
|
||||
public void setPointcutDeclarationScope(Class<?> pointcutDeclarationScope) {
|
||||
this.pointcutDeclarationScope = pointcutDeclarationScope;
|
||||
this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,6 +280,11 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
@Override
|
||||
public boolean matches(Class<?> targetClass) {
|
||||
if (this.pointcutParsingFailed) {
|
||||
// Pointcut parsing failed before below -> avoid trying again.
|
||||
return false;
|
||||
}
|
||||
if (this.aspectCompiledByAjc && compiledByAjc(targetClass)) {
|
||||
// ajc-compiled aspect class for ajc-compiled target class -> already weaved.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -536,6 +547,15 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
return resolveExpression().contains("@annotation");
|
||||
}
|
||||
|
||||
private static boolean compiledByAjc(Class<?> clazz) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.getName().startsWith(AJC_MAGIC)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
|
||||
+31
-19
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -22,9 +22,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.aspectj.lang.reflect.PerClauseKind;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -40,6 +43,8 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class BeanFactoryAspectJAdvisorsBuilder {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(BeanFactoryAspectJAdvisorsBuilder.class);
|
||||
|
||||
private final ListableBeanFactory beanFactory;
|
||||
|
||||
private final AspectJAdvisorFactory advisorFactory;
|
||||
@@ -102,30 +107,37 @@ public class BeanFactoryAspectJAdvisorsBuilder {
|
||||
continue;
|
||||
}
|
||||
if (this.advisorFactory.isAspect(beanType)) {
|
||||
aspectNames.add(beanName);
|
||||
AspectMetadata amd = new AspectMetadata(beanType, beanName);
|
||||
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
|
||||
MetadataAwareAspectInstanceFactory factory =
|
||||
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
|
||||
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
|
||||
if (this.beanFactory.isSingleton(beanName)) {
|
||||
this.advisorsCache.put(beanName, classAdvisors);
|
||||
try {
|
||||
AspectMetadata amd = new AspectMetadata(beanType, beanName);
|
||||
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
|
||||
MetadataAwareAspectInstanceFactory factory =
|
||||
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
|
||||
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
|
||||
if (this.beanFactory.isSingleton(beanName)) {
|
||||
this.advisorsCache.put(beanName, classAdvisors);
|
||||
}
|
||||
else {
|
||||
this.aspectFactoryCache.put(beanName, factory);
|
||||
}
|
||||
advisors.addAll(classAdvisors);
|
||||
}
|
||||
else {
|
||||
// Per target or per this.
|
||||
if (this.beanFactory.isSingleton(beanName)) {
|
||||
throw new IllegalArgumentException("Bean with name '" + beanName +
|
||||
"' is a singleton, but aspect instantiation model is not singleton");
|
||||
}
|
||||
MetadataAwareAspectInstanceFactory factory =
|
||||
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
|
||||
this.aspectFactoryCache.put(beanName, factory);
|
||||
advisors.addAll(this.advisorFactory.getAdvisors(factory));
|
||||
}
|
||||
advisors.addAll(classAdvisors);
|
||||
aspectNames.add(beanName);
|
||||
}
|
||||
else {
|
||||
// Per target or per this.
|
||||
if (this.beanFactory.isSingleton(beanName)) {
|
||||
throw new IllegalArgumentException("Bean with name '" + beanName +
|
||||
"' is a singleton, but aspect instantiation model is not singleton");
|
||||
catch (IllegalArgumentException | IllegalStateException | AopConfigException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Ignoring incompatible aspect [" + beanType.getName() + "]: " + ex);
|
||||
}
|
||||
MetadataAwareAspectInstanceFactory factory =
|
||||
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
|
||||
this.aspectFactoryCache.put(beanName, factory);
|
||||
advisors.addAll(this.advisorFactory.getAdvisors(factory));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -213,8 +213,16 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
|
||||
return null;
|
||||
}
|
||||
|
||||
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
|
||||
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
|
||||
try {
|
||||
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
|
||||
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
|
||||
}
|
||||
catch (IllegalArgumentException | IllegalStateException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Ignoring incompatible advice method: " + candidateAdviceMethod, ex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -488,20 +488,27 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
* @return a List of MethodInterceptors (may also include InterceptorAndDynamicMethodMatchers)
|
||||
*/
|
||||
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
|
||||
if (this.methodCache == null) {
|
||||
List<Object> cachedInterceptors;
|
||||
if (this.methodCache != null) {
|
||||
// Method-specific cache for method-specific pointcuts
|
||||
MethodCacheKey cacheKey = new MethodCacheKey(method);
|
||||
cachedInterceptors = this.methodCache.get(cacheKey);
|
||||
if (cachedInterceptors == null) {
|
||||
cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
|
||||
this, method, targetClass);
|
||||
this.methodCache.put(cacheKey, cachedInterceptors);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Shared cache since there are no method-specific advisors (see below).
|
||||
List<Object> cachedInterceptors = this.cachedInterceptors;
|
||||
cachedInterceptors = this.cachedInterceptors;
|
||||
if (cachedInterceptors == null) {
|
||||
cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
|
||||
this, method, targetClass);
|
||||
this.cachedInterceptors = cachedInterceptors;
|
||||
}
|
||||
return cachedInterceptors;
|
||||
}
|
||||
|
||||
// Method-specific cache for method-specific pointcuts
|
||||
return this.methodCache.computeIfAbsent(new MethodCacheKey(method), k ->
|
||||
this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(this, method, targetClass));
|
||||
return cachedInterceptors;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -20,7 +20,9 @@ import java.util.List;
|
||||
|
||||
import org.springframework.aop.Advisor;
|
||||
import org.springframework.aop.TargetSource;
|
||||
import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
@@ -97,7 +99,13 @@ public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyC
|
||||
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
|
||||
extendAdvisors(eligibleAdvisors);
|
||||
if (!eligibleAdvisors.isEmpty()) {
|
||||
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
|
||||
try {
|
||||
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
|
||||
}
|
||||
catch (BeanCreationException ex) {
|
||||
throw new AopConfigException("Advisor sorting failed with unexpected bean creation, probably due " +
|
||||
"to custom use of the Ordered interface. Consider using the @Order annotation instead.", ex);
|
||||
}
|
||||
}
|
||||
return eligibleAdvisors;
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -36,6 +36,7 @@ import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.refl
|
||||
* Tests for {@link AspectJAdvisorBeanRegistrationAotProcessor}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.1
|
||||
*/
|
||||
class AspectJAdvisorBeanRegistrationAotProcessorTests {
|
||||
|
||||
@@ -43,8 +44,9 @@ class AspectJAdvisorBeanRegistrationAotProcessorTests {
|
||||
|
||||
private final RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
|
||||
|
||||
|
||||
@Test
|
||||
void shouldProcessesAspectJClass() {
|
||||
void shouldProcessAspectJClass() {
|
||||
process(AspectJClass.class);
|
||||
assertThat(reflection().onType(AspectJClass.class).withMemberCategory(MemberCategory.DECLARED_FIELDS))
|
||||
.accepts(this.runtimeHints);
|
||||
|
||||
+10
-2
@@ -2,21 +2,29 @@
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:aop="http://www.springframework.org/schema/aop"
|
||||
xmlns:cache="http://www.springframework.org/schema/cache"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
|
||||
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd
|
||||
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache-3.1.xsd
|
||||
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd">
|
||||
|
||||
<aop:aspectj-autoproxy/>
|
||||
|
||||
<context:spring-configured/>
|
||||
|
||||
<cache:annotation-driven mode="aspectj"/>
|
||||
|
||||
<bean id="cacheManager" class="org.springframework.cache.support.NoOpCacheManager"/>
|
||||
|
||||
<bean id="myAspect" class="org.springframework.aop.aspectj.autoproxy.CodeStyleAspect" factory-method="aspectOf">
|
||||
<property name="foo" value="bar"/>
|
||||
</bean>
|
||||
|
||||
<bean id="otherBean" class="java.lang.Object"/>
|
||||
<bean id="otherBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring"/>
|
||||
|
||||
<bean id="yetAnotherBean" class="java.lang.Object"/>
|
||||
<bean id="yetAnotherBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring"/>
|
||||
|
||||
<bean id="configuredBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring" lazy-init="true"/>
|
||||
|
||||
</beans>
|
||||
|
||||
Vendored
+1
-2
@@ -24,8 +24,7 @@
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="defaultCache"
|
||||
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
|
||||
<bean id="defaultCache" class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
|
||||
<property name="name" value="default"/>
|
||||
</bean>
|
||||
|
||||
|
||||
+2
-4
@@ -7,12 +7,10 @@
|
||||
http://www.springframework.org/schema/task
|
||||
https://www.springframework.org/schema/task/spring-task.xsd">
|
||||
|
||||
<task:annotation-driven mode="aspectj" executor="testExecutor"
|
||||
exception-handler="testExceptionHandler"/>
|
||||
<task:annotation-driven mode="aspectj" executor="testExecutor" exception-handler="testExceptionHandler"/>
|
||||
|
||||
<task:executor id="testExecutor"/>
|
||||
|
||||
<bean id="testExceptionHandler"
|
||||
class="org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler"/>
|
||||
<bean id="testExceptionHandler" class="org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler"/>
|
||||
|
||||
</beans>
|
||||
|
||||
@@ -243,7 +243,7 @@ public interface BeanFactory {
|
||||
* specific type, specify the actual bean type as an argument here and subsequently
|
||||
* use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options.
|
||||
* <p>Also, generics matching is strict here, as per the Java assignment rules.
|
||||
* For lenient fallback matching with unchecked semantics (similar to the ´unchecked´
|
||||
* For lenient fallback matching with unchecked semantics (similar to the 'unchecked'
|
||||
* Java compiler warning), consider calling {@link #getBeanProvider(Class)} with the
|
||||
* raw type as a second step if no full generic match is
|
||||
* {@link ObjectProvider#getIfAvailable() available} with this variant.
|
||||
|
||||
@@ -42,6 +42,8 @@ import org.springframework.beans.propertyeditors.CustomDateEditor;
|
||||
import org.springframework.beans.testfixture.beans.DerivedTestBean;
|
||||
import org.springframework.beans.testfixture.beans.ITestBean;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.cglib.proxy.MethodInterceptor;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceEditor;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -322,12 +324,13 @@ class BeanUtilsTests {
|
||||
Order original = new Order("test", List.of("foo", "bar"));
|
||||
|
||||
// Create a Proxy that loses the generic type information for the getLineItems() method.
|
||||
OrderSummary proxy = proxyOrder(original);
|
||||
OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] {OrderSummary.class}, new OrderInvocationHandler(original));
|
||||
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
|
||||
.contains("java.util.List<java.lang.String>");
|
||||
.contains("java.util.List<java.lang.String>");
|
||||
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
|
||||
.contains("java.util.List")
|
||||
.doesNotContain("<java.lang.String>");
|
||||
.contains("java.util.List")
|
||||
.doesNotContain("<java.lang.String>");
|
||||
|
||||
// Ensure that our custom Proxy works as expected.
|
||||
assertThat(proxy.getId()).isEqualTo("test");
|
||||
@@ -340,6 +343,23 @@ class BeanUtilsTests {
|
||||
assertThat(target.getLineItems()).containsExactly("foo", "bar");
|
||||
}
|
||||
|
||||
@Test // gh-32888
|
||||
public void copyPropertiesWithGenericCglibClass() {
|
||||
Enhancer enhancer = new Enhancer();
|
||||
enhancer.setSuperclass(User.class);
|
||||
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
|
||||
User user = (User) enhancer.create();
|
||||
user.setId(1);
|
||||
user.setName("proxy");
|
||||
user.setAddress("addr");
|
||||
|
||||
User target = new User();
|
||||
BeanUtils.copyProperties(user, target);
|
||||
assertThat(target.getId()).isEqualTo(user.getId());
|
||||
assertThat(target.getName()).isEqualTo(user.getName());
|
||||
assertThat(target.getAddress()).isEqualTo(user.getAddress());
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyPropertiesWithEditable() throws Exception {
|
||||
TestBean tb = new TestBean();
|
||||
@@ -520,6 +540,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class IntegerHolder {
|
||||
|
||||
@@ -534,6 +555,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class WildcardListHolder1 {
|
||||
|
||||
@@ -548,6 +570,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class WildcardListHolder2 {
|
||||
|
||||
@@ -562,6 +585,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class NumberUpperBoundedWildcardListHolder {
|
||||
|
||||
@@ -576,6 +600,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class NumberListHolder {
|
||||
|
||||
@@ -590,6 +615,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class IntegerListHolder1 {
|
||||
|
||||
@@ -604,6 +630,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class IntegerListHolder2 {
|
||||
|
||||
@@ -618,6 +645,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class LongListHolder {
|
||||
|
||||
@@ -798,6 +826,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class BeanWithNullableTypes {
|
||||
|
||||
private Integer counter;
|
||||
@@ -828,6 +857,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class BeanWithPrimitiveTypes {
|
||||
|
||||
private boolean flag;
|
||||
@@ -840,7 +870,6 @@ class BeanUtilsTests {
|
||||
private char character;
|
||||
private String text;
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public BeanWithPrimitiveTypes(boolean flag, byte byteCount, short shortCount, int intCount, long longCount,
|
||||
float floatCount, double doubleCount, char character, String text) {
|
||||
@@ -891,21 +920,22 @@ class BeanUtilsTests {
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class PrivateBeanWithPrivateConstructor {
|
||||
|
||||
private PrivateBeanWithPrivateConstructor() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class Order {
|
||||
|
||||
private String id;
|
||||
private List<String> lineItems;
|
||||
|
||||
private List<String> lineItems;
|
||||
|
||||
Order() {
|
||||
}
|
||||
@@ -937,6 +967,7 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private interface OrderSummary {
|
||||
|
||||
String getId();
|
||||
@@ -945,17 +976,10 @@ class BeanUtilsTests {
|
||||
}
|
||||
|
||||
|
||||
private OrderSummary proxyOrder(Order order) {
|
||||
return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] { OrderSummary.class }, new OrderInvocationHandler(order));
|
||||
}
|
||||
|
||||
|
||||
private static class OrderInvocationHandler implements InvocationHandler {
|
||||
|
||||
private final Order order;
|
||||
|
||||
|
||||
OrderInvocationHandler(Order order) {
|
||||
this.order = order;
|
||||
}
|
||||
@@ -973,4 +997,46 @@ class BeanUtilsTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class GenericBaseModel<T> {
|
||||
|
||||
private T id;
|
||||
|
||||
private String name;
|
||||
|
||||
public T getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(T id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class User extends GenericBaseModel<Integer> {
|
||||
|
||||
private String address;
|
||||
|
||||
public User() {
|
||||
super();
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-5
@@ -21,9 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.composer.ComposerException;
|
||||
import org.yaml.snakeyaml.parser.ParserException;
|
||||
@@ -34,6 +32,7 @@ import org.springframework.core.io.ByteArrayResource;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.set;
|
||||
|
||||
/**
|
||||
* Tests for {@link YamlProcessor}.
|
||||
@@ -142,13 +141,11 @@ class YamlProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void standardTypesSupportedByDefault() {
|
||||
setYaml("value: !!set\n ? first\n ? second");
|
||||
this.processor.process((properties, map) -> {
|
||||
assertThat(properties).containsExactly(entry("value[0]", "first"), entry("value[1]", "second"));
|
||||
assertThat(map.get("value")).asInstanceOf(InstanceOfAssertFactories.type(Set.class))
|
||||
.satisfies(set -> assertThat(set).containsExactly("first", "second"));
|
||||
assertThat(map.get("value")).asInstanceOf(set(String.class)).containsExactly("first", "second");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -630,7 +630,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
if (result instanceof CompletableFuture<?> future) {
|
||||
return future.whenComplete((value, ex) -> {
|
||||
if (ex == null) {
|
||||
performCacheEvicts(applicable, result);
|
||||
performCacheEvicts(applicable, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1112,7 +1112,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
ReactiveAdapter adapter = (result != null ? this.registry.getAdapter(result.getClass()) : null);
|
||||
if (adapter != null) {
|
||||
return adapter.fromPublisher(Mono.from(adapter.toPublisher(result))
|
||||
.doOnSuccess(value -> performCacheEvicts(contexts, result)));
|
||||
.doOnSuccess(value -> performCacheEvicts(contexts, value)));
|
||||
}
|
||||
return NOT_HANDLED;
|
||||
}
|
||||
|
||||
+10
-1
@@ -49,6 +49,7 @@ import org.springframework.cglib.proxy.MethodProxy;
|
||||
import org.springframework.cglib.proxy.NoOp;
|
||||
import org.springframework.cglib.transform.ClassEmitterTransformer;
|
||||
import org.springframework.cglib.transform.TransformingClassGenerator;
|
||||
import org.springframework.core.SmartClassLoader;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.objenesis.ObjenesisException;
|
||||
import org.springframework.objenesis.SpringObjenesis;
|
||||
@@ -132,13 +133,21 @@ class ConfigurationClassEnhancer {
|
||||
enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
|
||||
enhancer.setUseFactory(false);
|
||||
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
|
||||
enhancer.setAttemptLoad(true);
|
||||
enhancer.setAttemptLoad(!isClassReloadable(configSuperClass, classLoader));
|
||||
enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
|
||||
enhancer.setCallbackFilter(CALLBACK_FILTER);
|
||||
enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
|
||||
return enhancer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given configuration class is reloadable.
|
||||
*/
|
||||
private boolean isClassReloadable(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
|
||||
return (classLoader instanceof SmartClassLoader smartClassLoader &&
|
||||
smartClassLoader.isClassReloadable(configSuperClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses enhancer to generate a subclass of superclass,
|
||||
* ensuring that callbacks are registered for the new subclass.
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* 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.
|
||||
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationEvent;
|
||||
public abstract class ApplicationContextEvent extends ApplicationEvent {
|
||||
|
||||
/**
|
||||
* Create a new ContextStartedEvent.
|
||||
* Create a new {@code ApplicationContextEvent}.
|
||||
* @param source the {@code ApplicationContext} that the event is raised for
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* 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.
|
||||
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationContext;
|
||||
public class ContextClosedEvent extends ApplicationContextEvent {
|
||||
|
||||
/**
|
||||
* Creates a new ContextClosedEvent.
|
||||
* Create a new {@code ContextClosedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been closed
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* 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.
|
||||
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationContext;
|
||||
public class ContextRefreshedEvent extends ApplicationContextEvent {
|
||||
|
||||
/**
|
||||
* Create a new ContextRefreshedEvent.
|
||||
* Create a new {@code ContextRefreshedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been initialized
|
||||
* or refreshed (must not be {@code null})
|
||||
*/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* 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.
|
||||
@@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext;
|
||||
public class ContextStartedEvent extends ApplicationContextEvent {
|
||||
|
||||
/**
|
||||
* Create a new ContextStartedEvent.
|
||||
* Create a new {@code ContextStartedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been started
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* 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.
|
||||
@@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext;
|
||||
public class ContextStoppedEvent extends ApplicationContextEvent {
|
||||
|
||||
/**
|
||||
* Create a new ContextStoppedEvent.
|
||||
* Create a new {@code ContextStoppedEvent}.
|
||||
* @param source the {@code ApplicationContext} that has been stopped
|
||||
* (must not be {@code null})
|
||||
*/
|
||||
|
||||
+10
-12
@@ -60,10 +60,6 @@ import org.springframework.util.ClassUtils;
|
||||
* groups for specific phases, on startup/shutdown as well as for explicit start/stop
|
||||
* interactions on a {@link org.springframework.context.ConfigurableApplicationContext}.
|
||||
*
|
||||
* <p>Provides interaction with {@link Lifecycle} and {@link SmartLifecycle} beans in
|
||||
* groups for specific phases, on startup/shutdown as well as for explicit start/stop
|
||||
* interactions on a {@link org.springframework.context.ConfigurableApplicationContext}.
|
||||
*
|
||||
* <p>As of 6.1, this also includes support for JVM checkpoint/restore (Project CRaC)
|
||||
* when the {@code org.crac:crac} dependency on the classpath.
|
||||
*
|
||||
@@ -98,7 +94,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
||||
public static final String ON_REFRESH_VALUE = "onRefresh";
|
||||
|
||||
|
||||
private static final boolean checkpointOnRefresh =
|
||||
private static boolean checkpointOnRefresh =
|
||||
ON_REFRESH_VALUE.equalsIgnoreCase(SpringProperties.getProperty(CHECKPOINT_PROPERTY_NAME));
|
||||
|
||||
private static final boolean exitOnRefresh =
|
||||
@@ -194,6 +190,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
if (checkpointOnRefresh) {
|
||||
checkpointOnRefresh = false;
|
||||
new CracDelegate().checkpointRestore();
|
||||
}
|
||||
if (exitOnRefresh) {
|
||||
@@ -249,13 +246,13 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
||||
|
||||
lifecycleBeans.forEach((beanName, bean) -> {
|
||||
if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
|
||||
int phase = getPhase(bean);
|
||||
phases.computeIfAbsent(
|
||||
phase,
|
||||
p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
|
||||
int startupPhase = getPhase(bean);
|
||||
phases.computeIfAbsent(startupPhase,
|
||||
phase -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
|
||||
).add(beanName, bean);
|
||||
}
|
||||
});
|
||||
|
||||
if (!phases.isEmpty()) {
|
||||
phases.values().forEach(LifecycleGroup::start);
|
||||
}
|
||||
@@ -306,13 +303,14 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
|
||||
private void stopBeans() {
|
||||
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
|
||||
Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
|
||||
|
||||
lifecycleBeans.forEach((beanName, bean) -> {
|
||||
int shutdownPhase = getPhase(bean);
|
||||
phases.computeIfAbsent(
|
||||
shutdownPhase,
|
||||
p -> new LifecycleGroup(shutdownPhase, this.timeoutPerShutdownPhase, lifecycleBeans, false)
|
||||
phases.computeIfAbsent(shutdownPhase,
|
||||
phase -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, false)
|
||||
).add(beanName, bean);
|
||||
});
|
||||
|
||||
if (!phases.isEmpty()) {
|
||||
phases.values().forEach(LifecycleGroup::stop);
|
||||
}
|
||||
|
||||
+27
-15
@@ -44,6 +44,7 @@ import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
|
||||
import org.springframework.beans.factory.config.SingletonBeanRegistry;
|
||||
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -155,6 +156,8 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
|
||||
private final Map<Object, List<Runnable>> reactiveSubscriptions = new IdentityHashMap<>(16);
|
||||
|
||||
private final Set<Object> manualCancellationOnContextClose = Collections.newSetFromMap(new IdentityHashMap<>(16));
|
||||
|
||||
|
||||
/**
|
||||
* Create a default {@code ScheduledAnnotationBeanPostProcessor}.
|
||||
@@ -305,6 +308,12 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
|
||||
"': " + annotatedMethods);
|
||||
}
|
||||
if ((this.beanFactory != null && !this.beanFactory.isSingleton(beanName)) ||
|
||||
(this.beanFactory instanceof SingletonBeanRegistry sbr && sbr.containsSingleton(beanName))) {
|
||||
// Either a prototype/scoped bean or a FactoryBean with a pre-existing managed singleton
|
||||
// -> trigger manual cancellation when ContextClosedEvent comes in
|
||||
this.manualCancellationOnContextClose.add(bean);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bean;
|
||||
@@ -595,6 +604,18 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
|
||||
@Override
|
||||
public void postProcessBeforeDestruction(Object bean, String beanName) {
|
||||
cancelScheduledTasks(bean);
|
||||
this.manualCancellationOnContextClose.remove(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresDestruction(Object bean) {
|
||||
synchronized (this.scheduledTasks) {
|
||||
return (this.scheduledTasks.containsKey(bean) || this.reactiveSubscriptions.containsKey(bean));
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelScheduledTasks(Object bean) {
|
||||
Set<ScheduledTask> tasks;
|
||||
List<Runnable> liveSubscriptions;
|
||||
synchronized (this.scheduledTasks) {
|
||||
@@ -613,13 +634,6 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresDestruction(Object bean) {
|
||||
synchronized (this.scheduledTasks) {
|
||||
return (this.scheduledTasks.containsKey(bean) || this.reactiveSubscriptions.containsKey(bean));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
synchronized (this.scheduledTasks) {
|
||||
@@ -636,7 +650,10 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
liveSubscription.run(); // equivalent to cancelling the subscription
|
||||
}
|
||||
}
|
||||
this.reactiveSubscriptions.clear();
|
||||
this.manualCancellationOnContextClose.clear();
|
||||
}
|
||||
|
||||
this.registrar.destroy();
|
||||
if (this.localScheduler != null) {
|
||||
this.localScheduler.destroy();
|
||||
@@ -659,15 +676,10 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
finishRegistration();
|
||||
}
|
||||
else if (event instanceof ContextClosedEvent) {
|
||||
synchronized (this.scheduledTasks) {
|
||||
Collection<Set<ScheduledTask>> allTasks = this.scheduledTasks.values();
|
||||
for (Set<ScheduledTask> tasks : allTasks) {
|
||||
for (ScheduledTask task : tasks) {
|
||||
// At this early point, let in-progress tasks complete still
|
||||
task.cancel(false);
|
||||
}
|
||||
}
|
||||
for (Object bean : this.manualCancellationOnContextClose) {
|
||||
cancelScheduledTasks(bean);
|
||||
}
|
||||
this.manualCancellationOnContextClose.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ public class DefaultBindingErrorProcessor implements BindingErrorProcessor {
|
||||
|
||||
@Override
|
||||
public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) {
|
||||
// Create field error with the exceptions's code, e.g. "typeMismatch".
|
||||
// Create field error with the code of the exception, e.g. "typeMismatch".
|
||||
String field = ex.getPropertyName();
|
||||
Assert.state(field != null, "No field in exception");
|
||||
String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field);
|
||||
|
||||
+13
-17
@@ -234,8 +234,8 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
|
||||
@Override
|
||||
public final MethodValidationResult validateArguments(
|
||||
Object target, Method method, @Nullable MethodParameter[] parameters, Object[] arguments,
|
||||
Class<?>[] groups) {
|
||||
Object target, Method method, @Nullable MethodParameter[] parameters,
|
||||
Object[] arguments, Class<?>[] groups) {
|
||||
|
||||
Set<ConstraintViolation<Object>> violations =
|
||||
invokeValidatorForArguments(target, method, arguments, groups);
|
||||
@@ -256,23 +256,21 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
Object target, Method method, Object[] arguments, Class<?>[] groups) {
|
||||
|
||||
ExecutableValidator execVal = this.validator.get().forExecutables();
|
||||
Set<ConstraintViolation<Object>> violations;
|
||||
try {
|
||||
violations = execVal.validateParameters(target, method, arguments, groups);
|
||||
return execVal.validateParameters(target, method, arguments, groups);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
|
||||
// Let's try to find the bridged method on the implementation class...
|
||||
Method bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(method, target.getClass());
|
||||
violations = execVal.validateParameters(target, bridgedMethod, arguments, groups);
|
||||
return execVal.validateParameters(target, bridgedMethod, arguments, groups);
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final MethodValidationResult validateReturnValue(
|
||||
Object target, Method method, @Nullable MethodParameter returnType, @Nullable Object returnValue,
|
||||
Class<?>[] groups) {
|
||||
Object target, Method method, @Nullable MethodParameter returnType,
|
||||
@Nullable Object returnValue, Class<?>[] groups) {
|
||||
|
||||
Set<ConstraintViolation<Object>> violations =
|
||||
invokeValidatorForReturnValue(target, method, returnValue, groups);
|
||||
@@ -305,9 +303,9 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
Map<Path.Node, ParamErrorsBuilder> nestedViolations = new LinkedHashMap<>();
|
||||
|
||||
for (ConstraintViolation<Object> violation : violations) {
|
||||
Iterator<Path.Node> itr = violation.getPropertyPath().iterator();
|
||||
while (itr.hasNext()) {
|
||||
Path.Node node = itr.next();
|
||||
Iterator<Path.Node> nodes = violation.getPropertyPath().iterator();
|
||||
while (nodes.hasNext()) {
|
||||
Path.Node node = nodes.next();
|
||||
|
||||
MethodParameter parameter;
|
||||
if (node.getKind().equals(ElementKind.PARAMETER)) {
|
||||
@@ -328,8 +326,8 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
// https://github.com/jakartaee/validation/issues/194
|
||||
|
||||
Path.Node parameterNode = node;
|
||||
if (itr.hasNext()) {
|
||||
node = itr.next();
|
||||
if (nodes.hasNext()) {
|
||||
node = nodes.next();
|
||||
}
|
||||
|
||||
Object value;
|
||||
@@ -425,7 +423,6 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
* @return the name to use
|
||||
*/
|
||||
String resolveName(MethodParameter parameter, @Nullable Object value);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -456,6 +453,7 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
public ParamValidationResultBuilder(
|
||||
Object target, MethodParameter parameter, @Nullable Object value, @Nullable Object container,
|
||||
@Nullable Integer containerIndex, @Nullable Object containerKey) {
|
||||
|
||||
this.target = target;
|
||||
this.parameter = parameter;
|
||||
this.value = value;
|
||||
@@ -473,7 +471,6 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
this.parameter, this.value, this.resolvableErrors, this.container,
|
||||
this.containerIndex, this.containerKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -527,8 +524,7 @@ public class MethodValidationAdapter implements MethodValidator {
|
||||
|
||||
|
||||
/**
|
||||
* Default algorithm to select an object name, as described in
|
||||
* {@link #setObjectNameResolver(ObjectNameResolver)}.
|
||||
* Default algorithm to select an object name, as described in {@link #setObjectNameResolver}.
|
||||
*/
|
||||
private static class DefaultObjectNameResolver implements ObjectNameResolver {
|
||||
|
||||
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -606,9 +606,9 @@ class CacheReproTests {
|
||||
return CompletableFuture.completedFuture(item);
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = "itemCache", allEntries = true)
|
||||
public CompletableFuture<Void> clear() {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
@CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0")
|
||||
public CompletableFuture<Integer> clear() {
|
||||
return CompletableFuture.completedFuture(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,9 +655,9 @@ class CacheReproTests {
|
||||
return Mono.just(item);
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = "itemCache", allEntries = true)
|
||||
public Mono<Void> clear() {
|
||||
return Mono.empty();
|
||||
@CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0")
|
||||
public Mono<Integer> clear() {
|
||||
return Mono.just(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,9 +706,9 @@ class CacheReproTests {
|
||||
return Flux.fromIterable(item);
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = "itemCache", allEntries = true)
|
||||
public Flux<Void> clear() {
|
||||
return Flux.empty();
|
||||
@CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0")
|
||||
public Flux<Integer> clear() {
|
||||
return Flux.just(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+8
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -51,7 +51,9 @@ class ReactiveCachingTests {
|
||||
LateCacheHitDeterminationConfig.class,
|
||||
LateCacheHitDeterminationWithValueWrapperConfig.class})
|
||||
void cacheHitDetermination(Class<?> configClass) {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
|
||||
|
||||
AnnotationConfigApplicationContext ctx =
|
||||
new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
|
||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||
|
||||
Object key = new Object();
|
||||
@@ -117,7 +119,9 @@ class ReactiveCachingTests {
|
||||
LateCacheHitDeterminationConfig.class,
|
||||
LateCacheHitDeterminationWithValueWrapperConfig.class})
|
||||
void fluxCacheDoesntDependOnFirstRequest(Class<?> configClass) {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
|
||||
|
||||
AnnotationConfigApplicationContext ctx =
|
||||
new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
|
||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||
|
||||
Object key = new Object();
|
||||
@@ -135,6 +139,7 @@ class ReactiveCachingTests {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
|
||||
@CacheConfig(cacheNames = "first")
|
||||
static class ReactiveCacheableService {
|
||||
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.context.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.SecureClassLoader;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.SmartClassLoader;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Phillip Webb
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class ConfigurationClassEnhancerTests {
|
||||
|
||||
@Test
|
||||
void enhanceReloadedClass() throws Exception {
|
||||
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
|
||||
ClassLoader parentClassLoader = getClass().getClassLoader();
|
||||
CustomClassLoader classLoader = new CustomClassLoader(parentClassLoader);
|
||||
Class<?> myClass = parentClassLoader.loadClass(MyConfig.class.getName());
|
||||
configurationClassEnhancer.enhance(myClass, parentClassLoader);
|
||||
Class<?> myReloadedClass = classLoader.loadClass(MyConfig.class.getName());
|
||||
Class<?> enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader);
|
||||
assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader);
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class MyConfig {
|
||||
|
||||
@Bean
|
||||
public String myBean() {
|
||||
return "bean";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class CustomClassLoader extends SecureClassLoader implements SmartClassLoader {
|
||||
|
||||
CustomClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
if (name.contains("MyConfig")) {
|
||||
String path = name.replace('.', '/').concat(".class");
|
||||
try (InputStream in = super.getResourceAsStream(path)) {
|
||||
byte[] bytes = StreamUtils.copyToByteArray(in);
|
||||
if (bytes.length > 0) {
|
||||
return defineClass(name, bytes, 0, bytes.length);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClassReloadable(Class<?> clazz) {
|
||||
return clazz.getName().contains("MyConfig");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -26,6 +26,7 @@ import jakarta.inject.Provider;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -43,6 +44,7 @@ import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -183,6 +185,14 @@ class AutowiredConfigurationTests {
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValueInjectionWithAccidentalAutowiredAnnotations() {
|
||||
AnnotationConfigApplicationContext context =
|
||||
new AnnotationConfigApplicationContext(ValueConfigWithAccidentalAutowiredAnnotations.class);
|
||||
doTestValueInjection(context);
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void doTestValueInjection(BeanFactory context) {
|
||||
System.clearProperty("myProp");
|
||||
|
||||
@@ -494,6 +504,32 @@ class AutowiredConfigurationTests {
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class ValueConfigWithAccidentalAutowiredAnnotations implements InitializingBean {
|
||||
|
||||
boolean invoked;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.state(!invoked, "Factory method must not get invoked on startup");
|
||||
}
|
||||
|
||||
@Bean @Scope("prototype")
|
||||
@Autowired
|
||||
public TestBean testBean(@Value("#{systemProperties[myProp]}") Provider<String> name) {
|
||||
invoked = true;
|
||||
return new TestBean(name.get());
|
||||
}
|
||||
|
||||
@Bean @Scope("prototype")
|
||||
@Autowired
|
||||
public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider<String> name2) {
|
||||
invoked = true;
|
||||
return new TestBean(name2.get());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class PropertiesConfig {
|
||||
|
||||
|
||||
+163
-13
@@ -21,18 +21,24 @@ import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.core.testfixture.EnabledForTestGroups;
|
||||
@@ -59,12 +65,20 @@ class EnableSchedulingTests {
|
||||
|
||||
private AnnotationConfigApplicationContext ctx;
|
||||
|
||||
private static final AtomicBoolean shutdownFailure = new AtomicBoolean();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void reset() {
|
||||
shutdownFailure.set(false);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (ctx != null) {
|
||||
ctx.close();
|
||||
}
|
||||
assertThat(shutdownFailure).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +89,7 @@ class EnableSchedulingTests {
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = {FixedRateTaskConfig.class, FixedRateTaskConfigSubclass.class})
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withFixedRateTask(Class<?> configClass) throws InterruptedException {
|
||||
void withFixedRateTask(Class<?> configClass) throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(configClass);
|
||||
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(2);
|
||||
|
||||
@@ -92,7 +106,7 @@ class EnableSchedulingTests {
|
||||
@ValueSource(classes = {ExplicitSchedulerConfig.class, ExplicitSchedulerConfigSubclass.class})
|
||||
@Timeout(2) // should actually complete within 1s
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withExplicitScheduler(Class<?> configClass) throws InterruptedException {
|
||||
void withExplicitScheduler(Class<?> configClass) throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(configClass);
|
||||
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
|
||||
|
||||
@@ -147,7 +161,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withExplicitScheduledTaskRegistrar() throws InterruptedException {
|
||||
void withExplicitScheduledTaskRegistrar() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(ExplicitScheduledTaskRegistrarConfig.class);
|
||||
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
|
||||
|
||||
@@ -158,7 +172,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withQualifiedScheduler() throws InterruptedException {
|
||||
void withQualifiedScheduler() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfig.class);
|
||||
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
|
||||
|
||||
@@ -169,7 +183,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withQualifiedSchedulerAndPlaceholder() throws InterruptedException {
|
||||
void withQualifiedSchedulerAndPlaceholder() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfigWithPlaceholder.class);
|
||||
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
|
||||
|
||||
@@ -181,7 +195,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withQualifiedSchedulerWithFixedDelayTask() throws InterruptedException {
|
||||
void withQualifiedSchedulerWithFixedDelayTask() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(QualifiedExplicitSchedulerConfigWithFixedDelayTask.class);
|
||||
assertThat(ctx.getBean(ScheduledTaskHolder.class).getScheduledTasks()).hasSize(1);
|
||||
|
||||
@@ -204,7 +218,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrarBean() throws InterruptedException {
|
||||
void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrarBean() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(
|
||||
SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedByScheduledTaskRegistrar.class);
|
||||
|
||||
@@ -214,7 +228,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute() throws InterruptedException {
|
||||
void withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(
|
||||
SchedulingEnabled_withAmbiguousTaskSchedulers_andSingleTask_disambiguatedBySchedulerNameAttribute.class);
|
||||
|
||||
@@ -224,7 +238,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withTaskAddedVia_configureTasks() throws InterruptedException {
|
||||
void withTaskAddedVia_configureTasks() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(SchedulingEnabled_withTaskAddedVia_configureTasks.class);
|
||||
|
||||
Thread.sleep(110);
|
||||
@@ -233,7 +247,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withInitiallyDelayedFixedRateTask() throws InterruptedException {
|
||||
void withInitiallyDelayedFixedRateTask() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(FixedRateTaskConfig_withInitialDelay.class);
|
||||
|
||||
Thread.sleep(1950);
|
||||
@@ -246,7 +260,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withInitiallyDelayedFixedDelayTask() throws InterruptedException {
|
||||
void withInitiallyDelayedFixedDelayTask() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withInitialDelay.class);
|
||||
|
||||
Thread.sleep(1950);
|
||||
@@ -259,7 +273,35 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withOneTimeTask() throws InterruptedException {
|
||||
void withPrototypeContainedFixedDelayTask() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withPrototypeBean.class);
|
||||
|
||||
ctx.getBean(PrototypeBeanWithScheduled.class);
|
||||
Thread.sleep(1950);
|
||||
AtomicInteger counter = ctx.getBean(AtomicInteger.class);
|
||||
|
||||
// The @Scheduled method should have been called several times
|
||||
// but not more times than the delay allows.
|
||||
assertThat(counter.get()).isBetween(1, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
void withPrototypeFactoryContainedFixedDelayTask() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(FixedDelayTaskConfig_withFactoryBean.class);
|
||||
|
||||
ctx.getBean(PrototypeBeanWithScheduled.class);
|
||||
Thread.sleep(1950);
|
||||
AtomicInteger counter = ctx.getBean(AtomicInteger.class);
|
||||
|
||||
// The @Scheduled method should have been called several times
|
||||
// but not more times than the delay allows.
|
||||
assertThat(counter.get()).isBetween(1, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
void withOneTimeTask() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(OneTimeTaskConfig.class);
|
||||
|
||||
Thread.sleep(110);
|
||||
@@ -271,7 +313,7 @@ class EnableSchedulingTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void withTriggerTask() throws InterruptedException {
|
||||
void withTriggerTask() throws InterruptedException {
|
||||
ctx = new AnnotationConfigApplicationContext(TriggerTaskConfig.class);
|
||||
|
||||
Thread.sleep(110);
|
||||
@@ -677,6 +719,9 @@ class EnableSchedulingTests {
|
||||
@EnableScheduling
|
||||
static class FixedRateTaskConfig_withInitialDelay {
|
||||
|
||||
@Autowired
|
||||
ScheduledAnnotationBeanPostProcessor bpp;
|
||||
|
||||
@Bean
|
||||
public AtomicInteger counter() {
|
||||
return new AtomicInteger();
|
||||
@@ -687,6 +732,13 @@ class EnableSchedulingTests {
|
||||
counter().incrementAndGet();
|
||||
Thread.sleep(100);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void validateLateCancellation() {
|
||||
if (this.bpp.getScheduledTasks().isEmpty()) {
|
||||
shutdownFailure.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -694,6 +746,9 @@ class EnableSchedulingTests {
|
||||
@EnableScheduling
|
||||
static class FixedDelayTaskConfig_withInitialDelay {
|
||||
|
||||
@Autowired
|
||||
ScheduledAnnotationBeanPostProcessor bpp;
|
||||
|
||||
@Bean
|
||||
public AtomicInteger counter() {
|
||||
return new AtomicInteger();
|
||||
@@ -704,6 +759,101 @@ class EnableSchedulingTests {
|
||||
counter().incrementAndGet();
|
||||
Thread.sleep(100);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void validateLateCancellation() {
|
||||
if (this.bpp.getScheduledTasks().isEmpty()) {
|
||||
shutdownFailure.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
static class FixedDelayTaskConfig_withPrototypeBean {
|
||||
|
||||
@Autowired
|
||||
ScheduledAnnotationBeanPostProcessor bpp;
|
||||
|
||||
@Bean
|
||||
public AtomicInteger counter() {
|
||||
return new AtomicInteger();
|
||||
}
|
||||
|
||||
@Bean @Scope("prototype")
|
||||
public PrototypeBeanWithScheduled prototypeBean() {
|
||||
return new PrototypeBeanWithScheduled(counter());
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void validateEarlyCancellation() {
|
||||
if (!this.bpp.getScheduledTasks().isEmpty()) {
|
||||
shutdownFailure.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
static class FixedDelayTaskConfig_withFactoryBean {
|
||||
|
||||
@Autowired
|
||||
ScheduledAnnotationBeanPostProcessor bpp;
|
||||
|
||||
@Bean
|
||||
public AtomicInteger counter() {
|
||||
return new AtomicInteger();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FactoryBeanForScheduled prototypeBean() {
|
||||
return new FactoryBeanForScheduled(counter());
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void validateEarlyCancellation() {
|
||||
if (!this.bpp.getScheduledTasks().isEmpty()) {
|
||||
shutdownFailure.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class PrototypeBeanWithScheduled {
|
||||
|
||||
private AtomicInteger counter;
|
||||
|
||||
public PrototypeBeanWithScheduled(AtomicInteger counter) {
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Scheduled(initialDelay = 1000, fixedDelay = 100)
|
||||
public void task() throws InterruptedException {
|
||||
this.counter.incrementAndGet();
|
||||
Thread.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class FactoryBeanForScheduled implements FactoryBean<PrototypeBeanWithScheduled> {
|
||||
|
||||
private AtomicInteger counter;
|
||||
|
||||
public FactoryBeanForScheduled(AtomicInteger counter) {
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrototypeBeanWithScheduled getObject() {
|
||||
return new PrototypeBeanWithScheduled(this.counter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return PrototypeBeanWithScheduled.class;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -352,9 +352,6 @@ class ScheduledAnnotationBeanPostProcessorTests {
|
||||
assertThat(condition).isTrue();
|
||||
CronTrigger cronTrigger = (CronTrigger) trigger;
|
||||
ZonedDateTime dateTime = ZonedDateTime.of(2013, 4, 15, 4, 0, 0, 0, ZoneId.of("GMT+10"));
|
||||
// Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+10"));
|
||||
// cal.clear();
|
||||
// cal.set(2013, 3, 15, 4, 0); // 15-04-2013 4:00 GMT+10;
|
||||
Instant lastScheduledExecution = dateTime.toInstant();
|
||||
Instant lastActualExecution = dateTime.toInstant();
|
||||
dateTime = dateTime.plusMinutes(30);
|
||||
@@ -1026,6 +1023,7 @@ class ScheduledAnnotationBeanPostProcessorTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class PropertyPlaceholderWithFixedDelayInSeconds {
|
||||
|
||||
@Scheduled(fixedDelayString = "${fixedDelay}", initialDelayString = "${initialDelay}", timeUnit = TimeUnit.SECONDS)
|
||||
@@ -1041,6 +1039,7 @@ class ScheduledAnnotationBeanPostProcessorTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class PropertyPlaceholderWithFixedRateInSeconds {
|
||||
|
||||
@Scheduled(fixedRateString = "${fixedRate}", initialDelayString = "${initialDelay}", timeUnit = TimeUnit.SECONDS)
|
||||
@@ -1071,9 +1070,11 @@ class ScheduledAnnotationBeanPostProcessorTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ConvertWith(NameToClass.Converter.class)
|
||||
private @interface NameToClass {
|
||||
|
||||
class Converter implements ArgumentConverter {
|
||||
@Override
|
||||
public Class<?> convert(Object beanClassName, ParameterContext context) throws ArgumentConversionException {
|
||||
|
||||
+1
-2
@@ -41,8 +41,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||
* // ...
|
||||
* }</pre>
|
||||
*
|
||||
* <p>The annotated element can be any Spring bean class, constructor, field,
|
||||
* or method — for example:
|
||||
* <p>The annotated element can be any Spring bean class or method — for example:
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Service
|
||||
|
||||
@@ -79,9 +79,9 @@ public final class BridgeMethodResolver {
|
||||
* method has been generated at the same class hierarchy level (a known difference
|
||||
* between the Eclipse compiler and regular javac).
|
||||
* @param bridgeMethod the method to introspect against the given target class
|
||||
* @param targetClass the target class to find methods on
|
||||
* @return the original method (either the bridged method or the passed-in method
|
||||
* if no more specific one could be found)
|
||||
* @param targetClass the target class to find the most specific method on
|
||||
* @return the most specific method corresponding to the given bridge method
|
||||
* (can be the original method if no more specific one could be found)
|
||||
* @since 6.1.3
|
||||
* @see #findBridgedMethod
|
||||
* @see org.springframework.util.ClassUtils#getMostSpecificMethod
|
||||
@@ -101,8 +101,12 @@ public final class BridgeMethodResolver {
|
||||
|
||||
private static Method resolveBridgeMethod(Method bridgeMethod, Class<?> targetClass) {
|
||||
boolean localBridge = (targetClass == bridgeMethod.getDeclaringClass());
|
||||
Class<?> userClass = targetClass;
|
||||
if (!bridgeMethod.isBridge() && localBridge) {
|
||||
return bridgeMethod;
|
||||
userClass = ClassUtils.getUserClass(targetClass);
|
||||
if (userClass == targetClass) {
|
||||
return bridgeMethod;
|
||||
}
|
||||
}
|
||||
|
||||
Object cacheKey = (localBridge ? bridgeMethod : new MethodClassKey(bridgeMethod, targetClass));
|
||||
@@ -111,7 +115,7 @@ public final class BridgeMethodResolver {
|
||||
// Gather all methods with matching name and parameter size.
|
||||
List<Method> candidateMethods = new ArrayList<>();
|
||||
MethodFilter filter = (candidateMethod -> isBridgedCandidateFor(candidateMethod, bridgeMethod));
|
||||
ReflectionUtils.doWithMethods(targetClass, candidateMethods::add, filter);
|
||||
ReflectionUtils.doWithMethods(userClass, candidateMethods::add, filter);
|
||||
if (!candidateMethods.isEmpty()) {
|
||||
bridgedMethod = (candidateMethods.size() == 1 ? candidateMethods.get(0) :
|
||||
searchCandidates(candidateMethods, bridgeMethod));
|
||||
@@ -272,8 +276,13 @@ public final class BridgeMethodResolver {
|
||||
*/
|
||||
public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) {
|
||||
if (bridgeMethod == bridgedMethod) {
|
||||
// Same method: for common purposes, return true to proceed as if it was a visibility bridge.
|
||||
return true;
|
||||
}
|
||||
if (ClassUtils.getUserClass(bridgeMethod.getDeclaringClass()) != bridgeMethod.getDeclaringClass()) {
|
||||
// Method on generated subclass: return false to consistently ignore it for visibility purposes.
|
||||
return false;
|
||||
}
|
||||
return (bridgeMethod.getReturnType().equals(bridgedMethod.getReturnType()) &&
|
||||
bridgeMethod.getParameterCount() == bridgedMethod.getParameterCount() &&
|
||||
Arrays.equals(bridgeMethod.getParameterTypes(), bridgedMethod.getParameterTypes()));
|
||||
|
||||
@@ -84,6 +84,10 @@ public abstract class KotlinDetector {
|
||||
/**
|
||||
* Determine whether the given {@code Class} is a Kotlin type
|
||||
* (with Kotlin metadata present on it).
|
||||
*
|
||||
* <p>As of Kotlin 2.0, this method can't be used to detect Kotlin
|
||||
* lambdas unless they are annotated with <code>@JvmSerializableLambda</code>
|
||||
* as invokedynamic has become the default method for lambda generation.
|
||||
*/
|
||||
public static boolean isKotlinType(Class<?> clazz) {
|
||||
return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null);
|
||||
|
||||
@@ -1052,16 +1052,16 @@ public abstract class AnnotationUtils {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Method method = annotation.annotationType().getDeclaredMethod(attributeName);
|
||||
return invokeAnnotationMethod(method, annotation);
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
return null;
|
||||
for (Method method : annotation.annotationType().getDeclaredMethods()) {
|
||||
if (method.getName().equals(attributeName) && method.getParameterCount() == 0) {
|
||||
return invokeAnnotationMethod(method, annotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
handleValueRetrievalFailure(annotation, ex);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -561,9 +561,8 @@ public class GenericConversionService implements ConfigurableConversionService {
|
||||
}
|
||||
|
||||
if (Enum.class.isAssignableFrom(type)) {
|
||||
addToClassHierarchy(hierarchy.size(), Enum.class, array, hierarchy, visited);
|
||||
addToClassHierarchy(hierarchy.size(), Enum.class, false, hierarchy, visited);
|
||||
addInterfacesToClassHierarchy(Enum.class, array, hierarchy, visited);
|
||||
addInterfacesToClassHierarchy(Enum.class, false, hierarchy, visited);
|
||||
}
|
||||
|
||||
addToClassHierarchy(hierarchy.size(), Object.class, array, hierarchy, visited);
|
||||
|
||||
@@ -58,6 +58,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Arjen Poutsma
|
||||
* @author Sam Brannen
|
||||
* @author Brian Clozel
|
||||
* @author Sebastien Deleuze
|
||||
* @since 16 April 2001
|
||||
*/
|
||||
public abstract class StringUtils {
|
||||
@@ -70,6 +71,8 @@ public abstract class StringUtils {
|
||||
|
||||
private static final String WINDOWS_FOLDER_SEPARATOR = "\\";
|
||||
|
||||
private static final String DOUBLE_BACKSLASHES = "\\\\";
|
||||
|
||||
private static final String TOP_PATH = "..";
|
||||
|
||||
private static final String CURRENT_PATH = ".";
|
||||
@@ -690,7 +693,7 @@ public abstract class StringUtils {
|
||||
* Normalize the path by suppressing sequences like "path/.." and
|
||||
* inner simple dots.
|
||||
* <p>The result is convenient for path comparison. For other uses,
|
||||
* notice that Windows separators ("\") are replaced by simple slashes.
|
||||
* notice that Windows separators ("\" and "\\") are replaced by simple slashes.
|
||||
* <p><strong>NOTE</strong> that {@code cleanPath} should not be depended
|
||||
* upon in a security context. Other mechanisms should be used to prevent
|
||||
* path-traversal issues.
|
||||
@@ -702,7 +705,15 @@ public abstract class StringUtils {
|
||||
return path;
|
||||
}
|
||||
|
||||
String normalizedPath = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
|
||||
String normalizedPath;
|
||||
// Optimize when there is no backslash
|
||||
if (path.indexOf('\\') != -1) {
|
||||
normalizedPath = replace(path, DOUBLE_BACKSLASHES, FOLDER_SEPARATOR);
|
||||
normalizedPath = replace(normalizedPath, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
|
||||
}
|
||||
else {
|
||||
normalizedPath = path;
|
||||
}
|
||||
String pathToUse = normalizedPath;
|
||||
|
||||
// Shortcut if there is no work to do
|
||||
|
||||
@@ -419,6 +419,7 @@ class StringUtilsTests {
|
||||
assertThat(StringUtils.cleanPath("file:///c:/some/../path/the%20file.txt")).isEqualTo("file:///c:/path/the%20file.txt");
|
||||
assertThat(StringUtils.cleanPath("jar:file:///c:\\some\\..\\path\\.\\the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt");
|
||||
assertThat(StringUtils.cleanPath("jar:file:///c:/some/../path/./the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt");
|
||||
assertThat(StringUtils.cleanPath("jar:file:///c:\\\\some\\\\..\\\\path\\\\.\\\\the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+11
-11
@@ -221,6 +221,8 @@ public class Indexer extends SpelNodeImpl {
|
||||
cf.loadTarget(mv);
|
||||
}
|
||||
|
||||
SpelNodeImpl index = this.children[0];
|
||||
|
||||
if (this.indexedType == IndexedType.ARRAY) {
|
||||
String exitTypeDescriptor = this.exitTypeDescriptor;
|
||||
Assert.state(exitTypeDescriptor != null, "Array not compilable without descriptor");
|
||||
@@ -266,18 +268,13 @@ public class Indexer extends SpelNodeImpl {
|
||||
}
|
||||
};
|
||||
|
||||
SpelNodeImpl index = this.children[0];
|
||||
cf.enterCompilationScope();
|
||||
index.generateCode(mv, cf);
|
||||
cf.exitCompilationScope();
|
||||
generateIndexCode(mv, cf, index, int.class);
|
||||
mv.visitInsn(insn);
|
||||
}
|
||||
|
||||
else if (this.indexedType == IndexedType.LIST) {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/util/List");
|
||||
cf.enterCompilationScope();
|
||||
this.children[0].generateCode(mv, cf);
|
||||
cf.exitCompilationScope();
|
||||
generateIndexCode(mv, cf, index, int.class);
|
||||
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true);
|
||||
}
|
||||
|
||||
@@ -285,14 +282,12 @@ public class Indexer extends SpelNodeImpl {
|
||||
mv.visitTypeInsn(CHECKCAST, "java/util/Map");
|
||||
// Special case when the key is an unquoted string literal that will be parsed as
|
||||
// a property/field reference
|
||||
if ((this.children[0] instanceof PropertyOrFieldReference reference)) {
|
||||
if ((index instanceof PropertyOrFieldReference reference)) {
|
||||
String mapKeyName = reference.getName();
|
||||
mv.visitLdcInsn(mapKeyName);
|
||||
}
|
||||
else {
|
||||
cf.enterCompilationScope();
|
||||
this.children[0].generateCode(mv, cf);
|
||||
cf.exitCompilationScope();
|
||||
generateIndexCode(mv, cf, index, Object.class);
|
||||
}
|
||||
mv.visitMethodInsn(
|
||||
INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true);
|
||||
@@ -328,6 +323,11 @@ public class Indexer extends SpelNodeImpl {
|
||||
cf.pushDescriptor(this.exitTypeDescriptor);
|
||||
}
|
||||
|
||||
private void generateIndexCode(MethodVisitor mv, CodeFlow cf, SpelNodeImpl indexNode, Class<?> indexType) {
|
||||
String indexDesc = CodeFlow.toDescriptor(indexType);
|
||||
generateCodeForArgument(mv, cf, indexNode, indexDesc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toStringAST() {
|
||||
return "[" + getChild(0).toStringAST() + "]";
|
||||
|
||||
+73
@@ -671,6 +671,79 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
|
||||
}
|
||||
|
||||
@Test // gh-32694, gh-32908
|
||||
void indexIntoArrayUsingIntegerWrapper() {
|
||||
context.setVariable("array", new int[] {1, 2, 3, 4});
|
||||
context.setVariable("index", 2);
|
||||
|
||||
expression = parser.parseExpression("#array[#index]");
|
||||
|
||||
assertThat(expression.getValue(context)).isEqualTo(3);
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo(3);
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo("I");
|
||||
}
|
||||
|
||||
@Test // gh-32694, gh-32908
|
||||
void indexIntoListUsingIntegerWrapper() {
|
||||
context.setVariable("list", List.of(1, 2, 3, 4));
|
||||
context.setVariable("index", 2);
|
||||
|
||||
expression = parser.parseExpression("#list[#index]");
|
||||
|
||||
assertThat(expression.getValue(context)).isEqualTo(3);
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo(3);
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
|
||||
}
|
||||
|
||||
@Test // gh-32903
|
||||
void indexIntoMapUsingPrimitiveLiteral() {
|
||||
Map<Object, String> map = Map.of(
|
||||
false, "0", // BooleanLiteral
|
||||
1, "ABC", // IntLiteral
|
||||
2L, "XYZ", // LongLiteral
|
||||
9.99F, "~10", // FloatLiteral
|
||||
3.14159, "PI" // RealLiteral
|
||||
);
|
||||
context.setVariable("map", map);
|
||||
|
||||
// BooleanLiteral
|
||||
expression = parser.parseExpression("#map[false]");
|
||||
assertThat(expression.getValue(context)).isEqualTo("0");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("0");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
|
||||
|
||||
// IntLiteral
|
||||
expression = parser.parseExpression("#map[1]");
|
||||
assertThat(expression.getValue(context)).isEqualTo("ABC");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("ABC");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
|
||||
|
||||
// LongLiteral
|
||||
expression = parser.parseExpression("#map[2L]");
|
||||
assertThat(expression.getValue(context)).isEqualTo("XYZ");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("XYZ");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
|
||||
|
||||
// FloatLiteral
|
||||
expression = parser.parseExpression("#map[9.99F]");
|
||||
assertThat(expression.getValue(context)).isEqualTo("~10");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("~10");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
|
||||
|
||||
// RealLiteral
|
||||
expression = parser.parseExpression("#map[3.14159]");
|
||||
assertThat(expression.getValue(context)).isEqualTo("PI");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(context)).isEqualTo("PI");
|
||||
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
|
||||
}
|
||||
|
||||
private String stringify(Object object) {
|
||||
Stream<? extends Object> stream;
|
||||
if (object instanceof Collection<?> collection) {
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ class AbstractRoutingDataSourceTests {
|
||||
routingDataSource.setDefaultTargetDataSource(ds);
|
||||
routingDataSource.setLenientFallback(false);
|
||||
routingDataSource.afterPropertiesSet();
|
||||
lookupKey.set(null);
|
||||
lookupKey.remove();
|
||||
assertThat(routingDataSource.determineTargetDataSource()).isSameAs(ds);
|
||||
}
|
||||
|
||||
|
||||
+46
-21
@@ -188,38 +188,63 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void contributeHibernateHints(RuntimeHints hints, @Nullable ClassLoader classLoader, Class<?> managedClass) {
|
||||
Class<? extends Annotation> embeddableInstantiatorClass = loadEmbeddableInstantiatorClass(classLoader);
|
||||
if (embeddableInstantiatorClass == null) {
|
||||
return;
|
||||
}
|
||||
ReflectionHints reflection = hints.reflection();
|
||||
registerInstantiatorForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(managedClass, embeddableInstantiatorClass));
|
||||
ReflectionUtils.doWithFields(managedClass, field -> {
|
||||
registerInstantiatorForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(field, embeddableInstantiatorClass));
|
||||
registerInstantiatorForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(field.getType(), embeddableInstantiatorClass));
|
||||
});
|
||||
}
|
||||
|
||||
private void registerInstantiatorForReflection(ReflectionHints reflection, @Nullable Annotation annotation) {
|
||||
if (annotation == null) {
|
||||
return;
|
||||
Class<? extends Annotation> embeddableInstantiatorClass = loadClass("org.hibernate.annotations.EmbeddableInstantiator", classLoader);
|
||||
if (embeddableInstantiatorClass != null) {
|
||||
registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(managedClass, embeddableInstantiatorClass), "value");
|
||||
ReflectionUtils.doWithFields(managedClass, field -> {
|
||||
registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(field, embeddableInstantiatorClass), "value");
|
||||
registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(field.getType(), embeddableInstantiatorClass), "value");
|
||||
});
|
||||
ReflectionUtils.doWithMethods(managedClass, method -> registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(method, embeddableInstantiatorClass), "value"));
|
||||
}
|
||||
|
||||
Class<? extends Annotation> valueGenerationTypeClass = loadClass("org.hibernate.annotations.ValueGenerationType", classLoader);
|
||||
if (valueGenerationTypeClass != null) {
|
||||
ReflectionUtils.doWithFields(managedClass, field -> registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(field, valueGenerationTypeClass), "generatedBy"));
|
||||
ReflectionUtils.doWithMethods(managedClass, method -> registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(method, valueGenerationTypeClass), "generatedBy"));
|
||||
}
|
||||
|
||||
Class<? extends Annotation> idGeneratorTypeClass = loadClass("org.hibernate.annotations.IdGeneratorType", classLoader);
|
||||
if (idGeneratorTypeClass != null) {
|
||||
ReflectionUtils.doWithFields(managedClass, field -> registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(field, idGeneratorTypeClass), "value"));
|
||||
ReflectionUtils.doWithMethods(managedClass, method -> registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(method, idGeneratorTypeClass), "value"));
|
||||
}
|
||||
|
||||
Class<? extends Annotation> attributeBinderTypeClass = loadClass("org.hibernate.annotations.AttributeBinderType", classLoader);
|
||||
if (attributeBinderTypeClass != null) {
|
||||
ReflectionUtils.doWithFields(managedClass, field -> registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(field, attributeBinderTypeClass), "binder"));
|
||||
ReflectionUtils.doWithMethods(managedClass, method -> registerForReflection(reflection,
|
||||
AnnotationUtils.findAnnotation(method, attributeBinderTypeClass), "binder"));
|
||||
}
|
||||
Class<?> embeddableInstantiatorClass = (Class<?>) AnnotationUtils.getAnnotationAttributes(annotation).get("value");
|
||||
reflection.registerType(embeddableInstantiatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Class<? extends Annotation> loadEmbeddableInstantiatorClass(@Nullable ClassLoader classLoader) {
|
||||
private static Class<? extends Annotation> loadClass(String className, @Nullable ClassLoader classLoader) {
|
||||
try {
|
||||
return (Class<? extends Annotation>) ClassUtils.forName(
|
||||
"org.hibernate.annotations.EmbeddableInstantiator", classLoader);
|
||||
return (Class<? extends Annotation>) ClassUtils.forName(className, classLoader);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void registerForReflection(ReflectionHints reflection, @Nullable Annotation annotation, String attribute) {
|
||||
if (annotation == null) {
|
||||
return;
|
||||
}
|
||||
Class<?> embeddableInstantiatorClass = (Class<?>) AnnotationUtils.getAnnotationAttributes(annotation).get(attribute);
|
||||
reflection.registerType(embeddableInstantiatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.orm.jpa.hibernate.domain;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
@Entity
|
||||
public class Book {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
private String title;
|
||||
|
||||
@CreationTimestamp
|
||||
private Instant createdOn;
|
||||
|
||||
public Book() {
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public Instant getCreatedOn() {
|
||||
return createdOn;
|
||||
}
|
||||
|
||||
public void setCreatedOn(Instant createdOn) {
|
||||
this.createdOn = createdOn;
|
||||
}
|
||||
}
|
||||
+36
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -21,6 +21,7 @@ import java.util.function.Consumer;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.hibernate.tuple.CreationTimestampGeneration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
@@ -30,7 +31,6 @@ import org.springframework.aot.test.generate.TestGenerationContext;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.aot.ApplicationContextAotGenerator;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
@@ -64,7 +64,7 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
|
||||
@Test
|
||||
void processEntityManagerWithPackagesToScan() {
|
||||
GenericApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.registerBean(EntityManagerWithPackagesToScanConfiguration.class);
|
||||
context.registerBean(JpaDomainConfiguration.class);
|
||||
compile(context, (initializer, compiled) -> {
|
||||
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(
|
||||
initializer);
|
||||
@@ -75,14 +75,14 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
|
||||
EmployeeLocationConverter.class.getName());
|
||||
assertThat(persistenceManagedTypes.getManagedPackages()).isEmpty();
|
||||
assertThat(freshApplicationContext.getBean(
|
||||
EntityManagerWithPackagesToScanConfiguration.class).scanningInvoked).isFalse();
|
||||
JpaDomainConfiguration.class).scanningInvoked).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void contributeHints() {
|
||||
void contributeJpaHints() {
|
||||
GenericApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.registerBean(EntityManagerWithPackagesToScanConfiguration.class);
|
||||
context.registerBean(JpaDomainConfiguration.class);
|
||||
contributeHints(context, hints -> {
|
||||
assertThat(RuntimeHintsPredicates.reflection().onType(DriversLicense.class)
|
||||
.withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints);
|
||||
@@ -108,6 +108,15 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void contributeHibernateHints() {
|
||||
GenericApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.registerBean(HibernateDomainConfiguration.class);
|
||||
contributeHints(context, hints ->
|
||||
assertThat(RuntimeHintsPredicates.reflection().onType(CreationTimestampGeneration.class)
|
||||
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(hints));
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void compile(GenericApplicationContext applicationContext,
|
||||
@@ -135,10 +144,25 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
|
||||
result.accept(generationContext.getRuntimeHints());
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public static class EntityManagerWithPackagesToScanConfiguration {
|
||||
public static class JpaDomainConfiguration extends AbstractEntityManagerWithPackagesToScanConfiguration {
|
||||
|
||||
private boolean scanningInvoked;
|
||||
@Override
|
||||
protected String packageToScan() {
|
||||
return "org.springframework.orm.jpa.domain";
|
||||
}
|
||||
}
|
||||
|
||||
public static class HibernateDomainConfiguration extends AbstractEntityManagerWithPackagesToScanConfiguration {
|
||||
|
||||
@Override
|
||||
protected String packageToScan() {
|
||||
return "org.springframework.orm.jpa.hibernate.domain";
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class AbstractEntityManagerWithPackagesToScanConfiguration {
|
||||
|
||||
protected boolean scanningInvoked;
|
||||
|
||||
@Bean
|
||||
public DataSource mockDataSource() {
|
||||
@@ -156,7 +180,7 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
|
||||
public PersistenceManagedTypes persistenceManagedTypes(ResourceLoader resourceLoader) {
|
||||
this.scanningInvoked = true;
|
||||
return new PersistenceManagedTypesScanner(resourceLoader)
|
||||
.scan("org.springframework.orm.jpa.domain");
|
||||
.scan(packageToScan());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -169,6 +193,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
|
||||
return entityManagerFactoryBean;
|
||||
}
|
||||
|
||||
protected abstract String packageToScan();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -20,6 +20,7 @@ import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanCreationNotAllowedException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
@@ -146,7 +147,14 @@ public class PersistenceExceptionTranslationInterceptor
|
||||
if (translator == null) {
|
||||
Assert.state(this.beanFactory != null,
|
||||
"Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory");
|
||||
translator = detectPersistenceExceptionTranslators(this.beanFactory);
|
||||
try {
|
||||
translator = detectPersistenceExceptionTranslators(this.beanFactory);
|
||||
}
|
||||
catch (BeanCreationNotAllowedException ex2) {
|
||||
// Cannot create PersistenceExceptionTranslator bean on shutdown:
|
||||
// fall back to rethrowing original exception without translation
|
||||
throw ex;
|
||||
}
|
||||
this.persistenceExceptionTranslator = translator;
|
||||
}
|
||||
throw DataAccessUtils.translateIfNecessary(ex, translator);
|
||||
|
||||
+30
-1
@@ -18,6 +18,7 @@ package org.springframework.dao.annotation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -29,6 +30,7 @@ import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.support.ChainedPersistenceExceptionTranslator;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslationInterceptor;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslator;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -78,10 +80,37 @@ class PersistenceExceptionTranslationInterceptorTests extends PersistenceExcepti
|
||||
given(invocation.proceed()).willThrow(exception);
|
||||
|
||||
assertThatThrownBy(() -> interceptor.invoke(invocation)).isSameAs(exception);
|
||||
|
||||
assertThat(callOrder).containsExactly(10, 20, 30);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectPersistenceExceptionTranslatorsOnShutdown() throws Throwable {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
|
||||
bf.registerBeanDefinition("peti", new RootBeanDefinition(PersistenceExceptionTranslationInterceptor.class));
|
||||
bf.registerBeanDefinition("pet", new RootBeanDefinition(ChainedPersistenceExceptionTranslator.class));
|
||||
|
||||
PersistenceExceptionTranslationInterceptor interceptor =
|
||||
bf.getBean("peti", PersistenceExceptionTranslationInterceptor.class);
|
||||
interceptor.setAlwaysTranslate(true);
|
||||
|
||||
RuntimeException exception = new RuntimeException();
|
||||
MethodInvocation invocation = mock();
|
||||
given(invocation.proceed()).willThrow(exception);
|
||||
|
||||
AtomicBoolean correctException = new AtomicBoolean(false);
|
||||
bf.registerDisposableBean("disposable", () -> {
|
||||
try {
|
||||
interceptor.invoke(invocation);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
correctException.set(ex == exception);
|
||||
}
|
||||
});
|
||||
bf.destroySingletons();
|
||||
assertThat(correctException).isTrue();
|
||||
}
|
||||
|
||||
|
||||
private static class CallOrderAwareExceptionTranslator implements PersistenceExceptionTranslator, Ordered {
|
||||
|
||||
|
||||
+6
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -73,6 +73,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
|
||||
|
||||
private long connectionRequestTimeout = -1;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new instance of the {@code HttpComponentsClientHttpRequestFactory}
|
||||
* with a default {@link HttpClient} based on system properties.
|
||||
@@ -202,6 +203,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
|
||||
this.httpContextFactory = httpContextFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
|
||||
HttpClient client = getHttpClient();
|
||||
@@ -309,8 +311,8 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* Template method that allows for manipulating the {@link ClassicHttpRequest} before it is
|
||||
* returned as part of a {@link HttpComponentsClientHttpRequest}.
|
||||
* Template method that allows for manipulating the {@link ClassicHttpRequest}
|
||||
* before it is returned as part of a {@link HttpComponentsClientHttpRequest}.
|
||||
* <p>The default implementation is empty.
|
||||
* @param request the request to process
|
||||
*/
|
||||
@@ -331,8 +333,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
|
||||
|
||||
|
||||
/**
|
||||
* Shutdown hook that closes the underlying
|
||||
* {@link HttpClientConnectionManager ClientConnectionManager}'s
|
||||
* Shutdown hook that closes the underlying {@link HttpClientConnectionManager}'s
|
||||
* connection pool, if any.
|
||||
*/
|
||||
@Override
|
||||
|
||||
+3
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023-2023 the original author or authors.
|
||||
* Copyright 2023-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.
|
||||
@@ -28,8 +28,7 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ClientHttpRequestFactory} implementation based on the Java
|
||||
* {@link HttpClient}.
|
||||
* {@link ClientHttpRequestFactory} implementation based on the Java {@link HttpClient}.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @author Arjen Poutsma
|
||||
@@ -89,13 +88,11 @@ public class JdkClientHttpRequestFactory implements ClientHttpRequestFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the underlying {@code HttpClient}'s read timeout as a
|
||||
* {@code Duration}.
|
||||
* Set the underlying {@code HttpClient}'s read timeout as a {@code Duration}.
|
||||
* <p>Default is the system's default timeout.
|
||||
* @see java.net.http.HttpRequest.Builder#timeout
|
||||
*/
|
||||
public void setReadTimeout(Duration readTimeout) {
|
||||
Assert.notNull(readTimeout, "ReadTimeout must not be null");
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
|
||||
|
||||
+20
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -42,6 +42,7 @@ import org.springframework.util.StreamUtils;
|
||||
* Created via the {@link ReactorNettyClientRequestFactory}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.1
|
||||
*/
|
||||
final class ReactorNettyClientRequest extends AbstractStreamingClientHttpRequest {
|
||||
@@ -101,18 +102,8 @@ final class ReactorNettyClientRequest extends AbstractStreamingClientHttpRequest
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (RuntimeException ex) { // Exceptions.ReactiveException is package private
|
||||
Throwable cause = ex.getCause();
|
||||
|
||||
if (cause instanceof UncheckedIOException uioEx) {
|
||||
throw uioEx.getCause();
|
||||
}
|
||||
else if (cause instanceof IOException ioEx) {
|
||||
throw ioEx;
|
||||
}
|
||||
else {
|
||||
throw ex;
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw convertException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +127,22 @@ final class ReactorNettyClientRequest extends AbstractStreamingClientHttpRequest
|
||||
}
|
||||
}
|
||||
|
||||
static IOException convertException(RuntimeException ex) {
|
||||
// Exceptions.ReactiveException is package private
|
||||
Throwable cause = ex.getCause();
|
||||
|
||||
if (cause instanceof IOException ioEx) {
|
||||
return ioEx;
|
||||
}
|
||||
if (cause instanceof UncheckedIOException uioEx) {
|
||||
IOException ioEx = uioEx.getCause();
|
||||
if (ioEx != null) {
|
||||
return ioEx;
|
||||
}
|
||||
}
|
||||
return new IOException(ex.getMessage(), cause);
|
||||
}
|
||||
|
||||
|
||||
private static final class ByteBufMapper implements OutputStreamPublisher.ByteMapper<ByteBuf> {
|
||||
|
||||
|
||||
+43
-49
@@ -50,19 +50,21 @@ public class ReactorNettyClientRequestFactory implements ClientHttpRequestFactor
|
||||
private static final Function<HttpClient, HttpClient> defaultInitializer = client -> client.compress(true);
|
||||
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
@Nullable
|
||||
private final ReactorResourceFactory resourceFactory;
|
||||
|
||||
@Nullable
|
||||
private final Function<HttpClient, HttpClient> mapper;
|
||||
|
||||
private Duration exchangeTimeout = Duration.ofSeconds(5);
|
||||
@Nullable
|
||||
private Integer connectTimeout;
|
||||
|
||||
private Duration readTimeout = Duration.ofSeconds(10);
|
||||
|
||||
private volatile boolean running = true;
|
||||
private Duration exchangeTimeout = Duration.ofSeconds(5);
|
||||
|
||||
@Nullable
|
||||
private volatile HttpClient httpClient;
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
@@ -107,25 +109,11 @@ public class ReactorNettyClientRequestFactory implements ClientHttpRequestFactor
|
||||
* @param mapper a mapper for further initialization of the created client
|
||||
*/
|
||||
public ReactorNettyClientRequestFactory(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
|
||||
this.httpClient = createHttpClient(resourceFactory, mapper);
|
||||
this.resourceFactory = resourceFactory;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
|
||||
private static HttpClient createHttpClient(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
|
||||
ConnectionProvider provider = resourceFactory.getConnectionProvider();
|
||||
Assert.notNull(provider, "No ConnectionProvider: is ReactorResourceFactory not initialized yet?");
|
||||
return defaultInitializer.andThen(mapper).andThen(applyLoopResources(resourceFactory))
|
||||
.apply(HttpClient.create(provider));
|
||||
}
|
||||
|
||||
private static Function<HttpClient, HttpClient> applyLoopResources(ReactorResourceFactory factory) {
|
||||
return httpClient -> {
|
||||
LoopResources resources = factory.getLoopResources();
|
||||
Assert.notNull(resources, "No LoopResources: is ReactorResourceFactory not initialized yet?");
|
||||
return httpClient.runOn(resources);
|
||||
};
|
||||
if (resourceFactory.isRunning()) {
|
||||
this.httpClient = createHttpClient(resourceFactory, mapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +126,11 @@ public class ReactorNettyClientRequestFactory implements ClientHttpRequestFactor
|
||||
*/
|
||||
public void setConnectTimeout(int connectTimeout) {
|
||||
Assert.isTrue(connectTimeout >= 0, "Timeout must be a non-negative value");
|
||||
this.httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout);
|
||||
this.connectTimeout = connectTimeout;
|
||||
HttpClient httpClient = this.httpClient;
|
||||
if (httpClient != null) {
|
||||
this.httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,8 +142,7 @@ public class ReactorNettyClientRequestFactory implements ClientHttpRequestFactor
|
||||
*/
|
||||
public void setConnectTimeout(Duration connectTimeout) {
|
||||
Assert.notNull(connectTimeout, "ConnectTimeout must not be null");
|
||||
Assert.isTrue(!connectTimeout.isNegative(), "Timeout must be a non-negative value");
|
||||
this.httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int)connectTimeout.toMillis());
|
||||
setConnectTimeout((int) connectTimeout.toMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,52 +183,55 @@ public class ReactorNettyClientRequestFactory implements ClientHttpRequestFactor
|
||||
this.exchangeTimeout = exchangeTimeout;
|
||||
}
|
||||
|
||||
private HttpClient createHttpClient(ReactorResourceFactory factory, Function<HttpClient, HttpClient> mapper) {
|
||||
HttpClient httpClient = defaultInitializer.andThen(mapper)
|
||||
.apply(HttpClient.create(factory.getConnectionProvider()));
|
||||
httpClient = httpClient.runOn(factory.getLoopResources());
|
||||
if (this.connectTimeout != null) {
|
||||
httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout);
|
||||
}
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
|
||||
return new ReactorNettyClientRequest(this.httpClient, uri, httpMethod, this.exchangeTimeout, this.readTimeout);
|
||||
HttpClient httpClient = this.httpClient;
|
||||
if (httpClient == null) {
|
||||
Assert.state(this.resourceFactory != null && this.mapper != null, "Illegal configuration");
|
||||
httpClient = createHttpClient(this.resourceFactory, this.mapper);
|
||||
}
|
||||
return new ReactorNettyClientRequest(httpClient, uri, httpMethod, this.exchangeTimeout, this.readTimeout);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (!isRunning()) {
|
||||
if (this.resourceFactory != null && this.mapper != null) {
|
||||
if (this.resourceFactory != null && this.mapper != null) {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (this.httpClient == null) {
|
||||
this.httpClient = createHttpClient(this.resourceFactory, this.mapper);
|
||||
}
|
||||
else {
|
||||
logger.warn("Restarting a ReactorNettyClientRequestFactory bean is only supported with externally managed Reactor Netty resources");
|
||||
}
|
||||
this.running = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.warn("Restarting a ReactorNettyClientRequestFactory bean is only supported " +
|
||||
"with externally managed Reactor Netty resources");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (isRunning()) {
|
||||
this.running = false;
|
||||
if (this.resourceFactory != null && this.mapper != null) {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
this.httpClient = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void stop(Runnable callback) {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
stop();
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return false;
|
||||
return (this.httpClient != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+9
-3
@@ -33,6 +33,7 @@ import org.springframework.util.StreamUtils;
|
||||
* {@link ClientHttpResponse} implementation for the Reactor-Netty HTTP client.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.1
|
||||
*/
|
||||
final class ReactorNettyClientResponse implements ClientHttpResponse {
|
||||
@@ -79,10 +80,15 @@ final class ReactorNettyClientResponse implements ClientHttpResponse {
|
||||
return body;
|
||||
}
|
||||
|
||||
body = this.connection.inbound().receive()
|
||||
.aggregate().asInputStream().block(this.readTimeout);
|
||||
try {
|
||||
body = this.connection.inbound().receive().aggregate().asInputStream().block(this.readTimeout);
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
throw ReactorNettyClientRequest.convertException(ex);
|
||||
}
|
||||
|
||||
if (body == null) {
|
||||
throw new IOException("Could not receive body");
|
||||
body = InputStream.nullInputStream();
|
||||
}
|
||||
this.body = body;
|
||||
return body;
|
||||
|
||||
+71
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -26,7 +26,9 @@ import reactor.netty.resources.LoopResources;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -35,20 +37,21 @@ import org.springframework.util.Assert;
|
||||
* event loop threads, and {@link ConnectionProvider} for the connection pool,
|
||||
* within the lifecycle of a Spring {@code ApplicationContext}.
|
||||
*
|
||||
* <p>This factory implements {@link InitializingBean}, {@link DisposableBean}
|
||||
* and {@link Lifecycle} and is expected typically to be declared as a
|
||||
* Spring-managed bean.
|
||||
* <p>This factory implements {@link SmartLifecycle} and is expected typically
|
||||
* to be declared as a Spring-managed bean.
|
||||
*
|
||||
* <p>Notice that after a {@link Lifecycle} stop/restart, new instances of
|
||||
* <p>Notice that after a {@link SmartLifecycle} stop/restart, new instances of
|
||||
* the configured {@link LoopResources} and {@link ConnectionProvider} are
|
||||
* created, so any references to those should be updated.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.1
|
||||
*/
|
||||
public class ReactorResourceFactory implements InitializingBean, DisposableBean, Lifecycle {
|
||||
public class ReactorResourceFactory
|
||||
implements ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle {
|
||||
|
||||
private boolean useGlobalResources = true;
|
||||
|
||||
@@ -58,12 +61,12 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean,
|
||||
private Supplier<ConnectionProvider> connectionProviderSupplier = () -> ConnectionProvider.create("webflux", 500);
|
||||
|
||||
@Nullable
|
||||
private ConnectionProvider connectionProvider;
|
||||
private volatile ConnectionProvider connectionProvider;
|
||||
|
||||
private Supplier<LoopResources> loopResourcesSupplier = () -> LoopResources.create("webflux-http");
|
||||
|
||||
@Nullable
|
||||
private LoopResources loopResources;
|
||||
private volatile LoopResources loopResources;
|
||||
|
||||
private boolean manageConnectionProvider = false;
|
||||
|
||||
@@ -73,6 +76,9 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean,
|
||||
|
||||
private Duration shutdownTimeout = Duration.ofSeconds(LoopResources.DEFAULT_SHUTDOWN_TIMEOUT);
|
||||
|
||||
@Nullable
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private volatile boolean running;
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
@@ -135,16 +141,22 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean,
|
||||
|
||||
/**
|
||||
* Return the configured {@link ConnectionProvider}.
|
||||
* <p>Lazily tries to start the resources on demand if not initialized yet.
|
||||
* @see #start()
|
||||
*/
|
||||
public ConnectionProvider getConnectionProvider() {
|
||||
Assert.state(this.connectionProvider != null, "ConnectionProvider not initialized yet");
|
||||
return this.connectionProvider;
|
||||
if (this.connectionProvider == null) {
|
||||
start();
|
||||
}
|
||||
ConnectionProvider connectionProvider = this.connectionProvider;
|
||||
Assert.state(connectionProvider != null, "ConnectionProvider not initialized");
|
||||
return connectionProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this when you don't want to participate in global resources and
|
||||
* you want to customize the creation of the managed {@code LoopResources}.
|
||||
* <p>By default, {@code LoopResources.create("reactor-http")} is used.
|
||||
* <p>By default, {@code LoopResources.create("webflux-http")} is used.
|
||||
* <p>Note that this option is ignored if {@code userGlobalResources=false} or
|
||||
* {@link #setLoopResources(LoopResources)} is set.
|
||||
* @param supplier the supplier to use
|
||||
@@ -164,10 +176,16 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean,
|
||||
|
||||
/**
|
||||
* Return the configured {@link LoopResources}.
|
||||
* <p>Lazily tries to start the resources on demand if not initialized yet.
|
||||
* @see #start()
|
||||
*/
|
||||
public LoopResources getLoopResources() {
|
||||
Assert.state(this.loopResources != null, "LoopResources not initialized yet");
|
||||
return this.loopResources;
|
||||
if (this.loopResources == null) {
|
||||
start();
|
||||
}
|
||||
LoopResources loopResources = this.loopResources;
|
||||
Assert.state(loopResources != null, "LoopResources not initialized");
|
||||
return loopResources;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,21 +220,48 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean,
|
||||
this.shutdownTimeout = shutdownTimeout;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setting an {@link ApplicationContext} is optional: If set, Reactor resources
|
||||
* will be initialized in the {@link #start() lifecycle start} phase and closed
|
||||
* in the {@link #stop() lifecycle stop} phase. If not set, it will happen in
|
||||
* {@link #afterPropertiesSet()} and {@link #destroy()}, respectively.
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
start();
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts the resources if initialized outside an ApplicationContext.
|
||||
* This is for backwards compatibility; the preferred way is to rely on
|
||||
* the ApplicationContext's {@link SmartLifecycle lifecycle management}.
|
||||
* @see #start()
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
if (this.applicationContext == null) {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the resources if initialized outside an ApplicationContext.
|
||||
* This is for backwards compatibility; the preferred way is to rely on
|
||||
* the ApplicationContext's {@link SmartLifecycle lifecycle management}.
|
||||
* @see #stop()
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
stop();
|
||||
if (this.applicationContext == null) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (!isRunning()) {
|
||||
if (!this.running) {
|
||||
if (this.useGlobalResources) {
|
||||
Assert.isTrue(this.loopResources == null && this.connectionProvider == null,
|
||||
"'useGlobalResources' is mutually exclusive with explicitly configured resources");
|
||||
@@ -246,7 +291,7 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean,
|
||||
@Override
|
||||
public void stop() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (isRunning()) {
|
||||
if (this.running) {
|
||||
if (this.useGlobalResources) {
|
||||
HttpResources.disposeLoopsAndConnectionsLater(this.shutdownQuietPeriod, this.shutdownTimeout).block();
|
||||
this.connectionProvider = null;
|
||||
@@ -285,4 +330,10 @@ public class ReactorResourceFactory implements InitializingBean, DisposableBean,
|
||||
return this.running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPhase() {
|
||||
// Same as plain Lifecycle
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+41
-60
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -54,24 +54,21 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
private static final Function<HttpClient, HttpClient> defaultInitializer = client -> client.compress(true);
|
||||
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
@Nullable
|
||||
private final ReactorResourceFactory resourceFactory;
|
||||
|
||||
@Nullable
|
||||
private final Function<HttpClient, HttpClient> mapper;
|
||||
|
||||
private volatile boolean running = true;
|
||||
@Nullable
|
||||
private volatile HttpClient httpClient;
|
||||
|
||||
private final Object lifecycleMonitor = new Object();
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor. Initializes {@link HttpClient} via:
|
||||
* <pre class="code">
|
||||
* HttpClient.create().compress()
|
||||
* </pre>
|
||||
* <pre class="code">HttpClient.create().compress(true)</pre>
|
||||
*/
|
||||
public ReactorClientHttpConnector() {
|
||||
this.httpClient = defaultInitializer.apply(HttpClient.create());
|
||||
@@ -79,6 +76,18 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
this.mapper = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with a pre-configured {@code HttpClient} instance.
|
||||
* @param httpClient the client to use
|
||||
* @since 5.1
|
||||
*/
|
||||
public ReactorClientHttpConnector(HttpClient httpClient) {
|
||||
Assert.notNull(httpClient, "HttpClient is required");
|
||||
this.httpClient = httpClient;
|
||||
this.resourceFactory = null;
|
||||
this.mapper = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with externally managed Reactor Netty resources, including
|
||||
* {@link LoopResources} for event loop threads, and {@link ConnectionProvider}
|
||||
@@ -98,37 +107,16 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
* @since 5.1
|
||||
*/
|
||||
public ReactorClientHttpConnector(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
|
||||
this.httpClient = createHttpClient(resourceFactory, mapper);
|
||||
this.resourceFactory = resourceFactory;
|
||||
this.mapper = mapper;
|
||||
if (resourceFactory.isRunning()) {
|
||||
this.httpClient = createHttpClient(resourceFactory, mapper);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient createHttpClient(ReactorResourceFactory resourceFactory, Function<HttpClient, HttpClient> mapper) {
|
||||
ConnectionProvider provider = resourceFactory.getConnectionProvider();
|
||||
Assert.notNull(provider, "No ConnectionProvider: is ReactorResourceFactory not initialized yet?");
|
||||
return defaultInitializer.andThen(mapper).andThen(applyLoopResources(resourceFactory))
|
||||
.apply(HttpClient.create(provider));
|
||||
}
|
||||
|
||||
private static Function<HttpClient, HttpClient> applyLoopResources(ReactorResourceFactory factory) {
|
||||
return httpClient -> {
|
||||
LoopResources resources = factory.getLoopResources();
|
||||
Assert.notNull(resources, "No LoopResources: is ReactorResourceFactory not initialized yet?");
|
||||
return httpClient.runOn(resources);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructor with a pre-configured {@code HttpClient} instance.
|
||||
* @param httpClient the client to use
|
||||
* @since 5.1
|
||||
*/
|
||||
public ReactorClientHttpConnector(HttpClient httpClient) {
|
||||
Assert.notNull(httpClient, "HttpClient is required");
|
||||
this.httpClient = httpClient;
|
||||
this.resourceFactory = null;
|
||||
this.mapper = null;
|
||||
private static HttpClient createHttpClient(ReactorResourceFactory factory, Function<HttpClient, HttpClient> mapper) {
|
||||
return defaultInitializer.andThen(mapper).andThen(httpClient -> httpClient.runOn(factory.getLoopResources()))
|
||||
.apply(HttpClient.create(factory.getConnectionProvider()));
|
||||
}
|
||||
|
||||
|
||||
@@ -136,12 +124,17 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
|
||||
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
|
||||
|
||||
AtomicReference<ReactorClientHttpResponse> responseRef = new AtomicReference<>();
|
||||
HttpClient httpClient = this.httpClient;
|
||||
if (httpClient == null) {
|
||||
Assert.state(this.resourceFactory != null && this.mapper != null, "Illegal configuration");
|
||||
httpClient = createHttpClient(this.resourceFactory, this.mapper);
|
||||
}
|
||||
|
||||
HttpClient.RequestSender requestSender = this.httpClient
|
||||
HttpClient.RequestSender requestSender = httpClient
|
||||
.request(io.netty.handler.codec.http.HttpMethod.valueOf(method.name()));
|
||||
|
||||
requestSender = setUri(requestSender, uri);
|
||||
AtomicReference<ReactorClientHttpResponse> responseRef = new AtomicReference<>();
|
||||
|
||||
return requestSender
|
||||
.send((request, outbound) -> requestCallback.apply(adaptRequest(method, uri, request, outbound)))
|
||||
@@ -176,46 +169,34 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif
|
||||
return new ReactorClientHttpRequest(method, uri, request, nettyOutbound);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (!isRunning()) {
|
||||
if (this.resourceFactory != null && this.mapper != null) {
|
||||
if (this.resourceFactory != null && this.mapper != null) {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (this.httpClient == null) {
|
||||
this.httpClient = createHttpClient(this.resourceFactory, this.mapper);
|
||||
}
|
||||
else {
|
||||
logger.warn("Restarting a ReactorClientHttpConnector bean is only supported with externally managed Reactor Netty resources");
|
||||
}
|
||||
this.running = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.warn("Restarting a ReactorClientHttpConnector bean is only supported " +
|
||||
"with externally managed Reactor Netty resources");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
if (isRunning()) {
|
||||
this.running = false;
|
||||
if (this.resourceFactory != null && this.mapper != null) {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
this.httpClient = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void stop(Runnable callback) {
|
||||
synchronized (this.lifecycleMonitor) {
|
||||
stop();
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return false;
|
||||
return (this.httpClient != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -135,7 +135,7 @@ public class ReactorNetty2ResourceFactory implements InitializingBean, Disposabl
|
||||
/**
|
||||
* Use this when you don't want to participate in global resources and
|
||||
* you want to customize the creation of the managed {@code LoopResources}.
|
||||
* <p>By default, {@code LoopResources.create("reactor-http")} is used.
|
||||
* <p>By default, {@code LoopResources.create("webflux-http")} is used.
|
||||
* <p>Note that this option is ignored if {@code userGlobalResources=false} or
|
||||
* {@link #setLoopResources(LoopResources)} is set.
|
||||
* @param supplier the supplier to use
|
||||
@@ -170,7 +170,6 @@ public class ReactorNetty2ResourceFactory implements InitializingBean, Disposabl
|
||||
* can also be overridden with the system property
|
||||
* {@link reactor.netty5.ReactorNetty#SHUTDOWN_QUIET_PERIOD
|
||||
* ReactorNetty.SHUTDOWN_QUIET_PERIOD}.
|
||||
* @since 5.2.4
|
||||
* @see #setShutdownTimeout(Duration)
|
||||
*/
|
||||
public void setShutdownQuietPeriod(Duration shutdownQuietPeriod) {
|
||||
@@ -187,7 +186,6 @@ public class ReactorNetty2ResourceFactory implements InitializingBean, Disposabl
|
||||
* can also be overridden with the system property
|
||||
* {@link reactor.netty5.ReactorNetty#SHUTDOWN_TIMEOUT
|
||||
* ReactorNetty.SHUTDOWN_TIMEOUT}.
|
||||
* @since 5.2.4
|
||||
* @see #setShutdownQuietPeriod(Duration)
|
||||
*/
|
||||
public void setShutdownTimeout(Duration shutdownTimeout) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* 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.
|
||||
@@ -92,7 +92,8 @@ public class Jaxb2XmlEncoder extends AbstractSingleValueEncoder<Object> {
|
||||
if (super.canEncode(elementType, mimeType)) {
|
||||
Class<?> outputClass = elementType.toClass();
|
||||
return (outputClass.isAnnotationPresent(XmlRootElement.class) ||
|
||||
outputClass.isAnnotationPresent(XmlType.class));
|
||||
outputClass.isAnnotationPresent(XmlType.class) ||
|
||||
elementType.isAssignableFrom(JAXBElement.class));
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
|
||||
@@ -240,19 +240,16 @@ final class DefaultRestClient implements RestClient {
|
||||
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", cause);
|
||||
if (observation != null) {
|
||||
observation.error(restClientException);
|
||||
observation.stop();
|
||||
}
|
||||
throw restClientException;
|
||||
}
|
||||
catch (RestClientException restClientException) {
|
||||
if (observation != null) {
|
||||
observation.error(restClientException);
|
||||
}
|
||||
throw restClientException;
|
||||
}
|
||||
finally {
|
||||
if (observation != null) {
|
||||
observation.stop();
|
||||
}
|
||||
throw restClientException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* 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.
|
||||
@@ -21,9 +21,12 @@ import java.util.Optional;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import jakarta.servlet.AsyncEvent;
|
||||
import jakarta.servlet.AsyncListener;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -94,11 +97,6 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
|
||||
return Optional.ofNullable((ServerRequestObservationContext) request.getAttribute(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilterAsyncDispatch() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("try")
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
@@ -114,8 +112,12 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
|
||||
throw ex;
|
||||
}
|
||||
finally {
|
||||
// Only stop Observation if async processing is done or has never been started.
|
||||
if (!request.isAsyncStarted()) {
|
||||
// If async is started, register a listener for completion notification.
|
||||
if (request.isAsyncStarted()) {
|
||||
request.getAsyncContext().addListener(new ObservationAsyncListener(observation));
|
||||
}
|
||||
// Stop Observation right now if async processing has not been started.
|
||||
else {
|
||||
Throwable error = fetchException(request);
|
||||
if (error != null) {
|
||||
observation.error(error);
|
||||
@@ -140,13 +142,43 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Throwable unwrapServletException(Throwable ex) {
|
||||
static Throwable unwrapServletException(Throwable ex) {
|
||||
return (ex instanceof ServletException) ? ex.getCause() : ex;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Throwable fetchException(HttpServletRequest request) {
|
||||
static Throwable fetchException(ServletRequest request) {
|
||||
return (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
|
||||
}
|
||||
|
||||
private static class ObservationAsyncListener implements AsyncListener {
|
||||
|
||||
private final Observation currentObservation;
|
||||
|
||||
public ObservationAsyncListener(Observation currentObservation) {
|
||||
this.currentObservation = currentObservation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartAsync(AsyncEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(AsyncEvent event) {
|
||||
this.currentObservation.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(AsyncEvent event) {
|
||||
this.currentObservation.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(AsyncEvent event) {
|
||||
this.currentObservation.error(unwrapServletException(event.getThrowable()));
|
||||
this.currentObservation.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -417,7 +417,7 @@ public class HandlerMethod extends AnnotatedMethod {
|
||||
return true;
|
||||
}
|
||||
merged = MergedAnnotations.from(getContainerElementAnnotations(param));
|
||||
if (merged.stream().anyMatch(CONSTRAINT_PREDICATE)) {
|
||||
if (merged.stream().anyMatch(CONSTRAINT_PREDICATE.or(VALID_PREDICATE))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -37,7 +37,6 @@ import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.HttpSessionRequiredException;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
@@ -115,7 +114,7 @@ public final class ModelFactory {
|
||||
if (!container.containsAttribute(name)) {
|
||||
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
|
||||
if (value == null) {
|
||||
throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
|
||||
throw new IllegalStateException("Expected session attribute '" + name + "'");
|
||||
}
|
||||
container.addAttribute(name, value);
|
||||
}
|
||||
|
||||
+6
-1
@@ -89,7 +89,12 @@ public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
|
||||
public ContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit) {
|
||||
super(request);
|
||||
int contentLength = request.getContentLength();
|
||||
this.cachedContent = (contentLength > 0) ? new FastByteArrayOutputStream(contentLength) : new FastByteArrayOutputStream();
|
||||
if (contentLength > 0) {
|
||||
this.cachedContent = new FastByteArrayOutputStream(Math.min(contentLength, contentCacheLimit));
|
||||
}
|
||||
else {
|
||||
this.cachedContent = new FastByteArrayOutputStream();
|
||||
}
|
||||
this.contentCacheLimit = contentCacheLimit;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import org.springframework.util.Assert;
|
||||
public class DisconnectedClientHelper {
|
||||
|
||||
private static final Set<String> EXCEPTION_PHRASES =
|
||||
Set.of("broken pipe", "connection reset by peer");
|
||||
Set.of("broken pipe", "connection reset");
|
||||
|
||||
private static final Set<String> EXCEPTION_TYPE_NAMES =
|
||||
Set.of("AbortedException", "ClientAbortException",
|
||||
@@ -73,11 +73,12 @@ public class DisconnectedClientHelper {
|
||||
|
||||
/**
|
||||
* Whether the given exception indicates the client has gone away.
|
||||
* Known cases covered:
|
||||
* <p>Known cases covered:
|
||||
* <ul>
|
||||
* <li>ClientAbortException or EOFException for Tomcat
|
||||
* <li>EofException for Jetty
|
||||
* <li>IOException "Broken pipe" or "connection reset by peer"
|
||||
* <li>SocketException "Connection reset"
|
||||
* </ul>
|
||||
*/
|
||||
public static boolean isClientDisconnectedException(Throwable ex) {
|
||||
|
||||
+22
-6
@@ -28,8 +28,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.1
|
||||
*/
|
||||
class ReactorNettyClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests {
|
||||
class ReactorNettyClientRequestFactoryTests extends AbstractHttpRequestFactoryTests {
|
||||
|
||||
@Override
|
||||
protected ClientHttpRequestFactory createRequestFactory() {
|
||||
@@ -50,7 +51,20 @@ class ReactorNettyClientHttpRequestFactoryTests extends AbstractHttpRequestFacto
|
||||
requestFactory.start();
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
requestFactory.stop();
|
||||
assertThat(requestFactory.isRunning()).isFalse();
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
requestFactory.start();
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartWithHttpClient() {
|
||||
HttpClient httpClient = HttpClient.create();
|
||||
ReactorNettyClientRequestFactory requestFactory = new ReactorNettyClientRequestFactory(httpClient);
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
requestFactory.start();
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
requestFactory.stop();
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
requestFactory.start();
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
}
|
||||
@@ -71,10 +85,12 @@ class ReactorNettyClientHttpRequestFactoryTests extends AbstractHttpRequestFacto
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartWithHttpClient() {
|
||||
HttpClient httpClient = HttpClient.create();
|
||||
ReactorNettyClientRequestFactory requestFactory = new ReactorNettyClientRequestFactory(httpClient);
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
void lateStartWithExternalResourceFactory() {
|
||||
ReactorResourceFactory resourceFactory = new ReactorResourceFactory();
|
||||
Function<HttpClient, HttpClient> mapper = Function.identity();
|
||||
ReactorNettyClientRequestFactory requestFactory = new ReactorNettyClientRequestFactory(resourceFactory, mapper);
|
||||
assertThat(requestFactory.isRunning()).isFalse();
|
||||
resourceFactory.start();
|
||||
requestFactory.start();
|
||||
assertThat(requestFactory.isRunning()).isTrue();
|
||||
requestFactory.stop();
|
||||
+84
-31
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.http.client.reactive;
|
||||
package org.springframework.http.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -24,7 +24,7 @@ import reactor.netty.http.HttpResources;
|
||||
import reactor.netty.resources.ConnectionProvider;
|
||||
import reactor.netty.resources.LoopResources;
|
||||
|
||||
import org.springframework.http.client.ReactorResourceFactory;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@@ -37,6 +37,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class ReactorResourceFactoryTests {
|
||||
|
||||
@@ -49,37 +50,34 @@ class ReactorResourceFactoryTests {
|
||||
|
||||
@Test
|
||||
void globalResources() {
|
||||
|
||||
this.resourceFactory.setUseGlobalResources(true);
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.start();
|
||||
|
||||
HttpResources globalResources = HttpResources.get();
|
||||
assertThat(this.resourceFactory.getConnectionProvider()).isSameAs(globalResources);
|
||||
assertThat(this.resourceFactory.getLoopResources()).isSameAs(globalResources);
|
||||
assertThat(globalResources.isDisposed()).isFalse();
|
||||
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.stop();
|
||||
|
||||
assertThat(globalResources.isDisposed()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalResourcesWithConsumer() {
|
||||
|
||||
AtomicBoolean invoked = new AtomicBoolean();
|
||||
|
||||
this.resourceFactory.addGlobalResourcesConsumer(httpResources -> invoked.set(true));
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.start();
|
||||
|
||||
assertThat(invoked.get()).isTrue();
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void localResources() {
|
||||
|
||||
this.resourceFactory.setUseGlobalResources(false);
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.start();
|
||||
|
||||
ConnectionProvider connectionProvider = this.resourceFactory.getConnectionProvider();
|
||||
LoopResources loopResources = this.resourceFactory.getLoopResources();
|
||||
@@ -91,7 +89,7 @@ class ReactorResourceFactoryTests {
|
||||
// assertFalse(connectionProvider.isDisposed());
|
||||
assertThat(loopResources.isDisposed()).isFalse();
|
||||
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.stop();
|
||||
|
||||
assertThat(connectionProvider.isDisposed()).isTrue();
|
||||
assertThat(loopResources.isDisposed()).isTrue();
|
||||
@@ -99,11 +97,10 @@ class ReactorResourceFactoryTests {
|
||||
|
||||
@Test
|
||||
void localResourcesViaSupplier() {
|
||||
|
||||
this.resourceFactory.setUseGlobalResources(false);
|
||||
this.resourceFactory.setConnectionProviderSupplier(() -> this.connectionProvider);
|
||||
this.resourceFactory.setLoopResourcesSupplier(() -> this.loopResources);
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.start();
|
||||
|
||||
ConnectionProvider connectionProvider = this.resourceFactory.getConnectionProvider();
|
||||
LoopResources loopResources = this.resourceFactory.getLoopResources();
|
||||
@@ -113,9 +110,9 @@ class ReactorResourceFactoryTests {
|
||||
|
||||
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
|
||||
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.stop();
|
||||
|
||||
// Managed (destroy disposes)..
|
||||
// Managed (stop disposes)..
|
||||
verify(this.connectionProvider).disposeLater();
|
||||
verify(this.loopResources).disposeLater(eq(Duration.ofSeconds(LoopResources.DEFAULT_SHUTDOWN_QUIET_PERIOD)), eq(Duration.ofSeconds(LoopResources.DEFAULT_SHUTDOWN_TIMEOUT)));
|
||||
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
|
||||
@@ -130,8 +127,8 @@ class ReactorResourceFactoryTests {
|
||||
this.resourceFactory.setLoopResourcesSupplier(() -> this.loopResources);
|
||||
this.resourceFactory.setShutdownQuietPeriod(quietPeriod);
|
||||
this.resourceFactory.setShutdownTimeout(shutdownTimeout);
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.start();
|
||||
this.resourceFactory.stop();
|
||||
|
||||
verify(this.connectionProvider).disposeLater();
|
||||
verify(this.loopResources).disposeLater(eq(quietPeriod), eq(shutdownTimeout));
|
||||
@@ -140,11 +137,10 @@ class ReactorResourceFactoryTests {
|
||||
|
||||
@Test
|
||||
void externalResources() {
|
||||
|
||||
this.resourceFactory.setUseGlobalResources(false);
|
||||
this.resourceFactory.setConnectionProvider(this.connectionProvider);
|
||||
this.resourceFactory.setLoopResources(this.loopResources);
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.start();
|
||||
|
||||
ConnectionProvider connectionProvider = this.resourceFactory.getConnectionProvider();
|
||||
LoopResources loopResources = this.resourceFactory.getLoopResources();
|
||||
@@ -154,17 +150,16 @@ class ReactorResourceFactoryTests {
|
||||
|
||||
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
|
||||
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.stop();
|
||||
|
||||
// Not managed (destroy has no impact)
|
||||
// Not managed (stop has no impact)
|
||||
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartWithGlobalResources() {
|
||||
|
||||
this.resourceFactory.setUseGlobalResources(true);
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.start();
|
||||
this.resourceFactory.stop();
|
||||
this.resourceFactory.start();
|
||||
|
||||
@@ -173,16 +168,15 @@ class ReactorResourceFactoryTests {
|
||||
assertThat(this.resourceFactory.getLoopResources()).isSameAs(globalResources);
|
||||
assertThat(globalResources.isDisposed()).isFalse();
|
||||
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.stop();
|
||||
|
||||
assertThat(globalResources.isDisposed()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartWithLocalResources() {
|
||||
|
||||
this.resourceFactory.setUseGlobalResources(false);
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.start();
|
||||
this.resourceFactory.stop();
|
||||
this.resourceFactory.start();
|
||||
|
||||
@@ -196,7 +190,7 @@ class ReactorResourceFactoryTests {
|
||||
// assertFalse(connectionProvider.isDisposed());
|
||||
assertThat(loopResources.isDisposed()).isFalse();
|
||||
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.stop();
|
||||
|
||||
assertThat(connectionProvider.isDisposed()).isTrue();
|
||||
assertThat(loopResources.isDisposed()).isTrue();
|
||||
@@ -204,11 +198,10 @@ class ReactorResourceFactoryTests {
|
||||
|
||||
@Test
|
||||
void restartWithExternalResources() {
|
||||
|
||||
this.resourceFactory.setUseGlobalResources(false);
|
||||
this.resourceFactory.setConnectionProvider(this.connectionProvider);
|
||||
this.resourceFactory.setLoopResources(this.loopResources);
|
||||
this.resourceFactory.afterPropertiesSet();
|
||||
this.resourceFactory.start();
|
||||
this.resourceFactory.stop();
|
||||
this.resourceFactory.start();
|
||||
|
||||
@@ -220,10 +213,70 @@ class ReactorResourceFactoryTests {
|
||||
|
||||
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
|
||||
|
||||
this.resourceFactory.destroy();
|
||||
this.resourceFactory.stop();
|
||||
|
||||
// Not managed (destroy has no impact)...
|
||||
// Not managed (stop has no impact)...
|
||||
verifyNoMoreInteractions(this.connectionProvider, this.loopResources);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartWithinApplicationContext() {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.registerBean(ReactorResourceFactory.class);
|
||||
context.refresh();
|
||||
|
||||
ReactorResourceFactory resourceFactory = context.getBean(ReactorResourceFactory.class);
|
||||
assertThat(resourceFactory.isRunning()).isTrue();
|
||||
|
||||
HttpResources globalResources = HttpResources.get();
|
||||
assertThat(resourceFactory.getConnectionProvider()).isSameAs(globalResources);
|
||||
assertThat(resourceFactory.getLoopResources()).isSameAs(globalResources);
|
||||
assertThat(globalResources.isDisposed()).isFalse();
|
||||
|
||||
context.stop();
|
||||
assertThat(globalResources.isDisposed()).isTrue();
|
||||
|
||||
context.start();
|
||||
globalResources = HttpResources.get();
|
||||
assertThat(resourceFactory.getConnectionProvider()).isSameAs(globalResources);
|
||||
assertThat(resourceFactory.getLoopResources()).isSameAs(globalResources);
|
||||
assertThat(globalResources.isDisposed()).isFalse();
|
||||
assertThat(globalResources.isDisposed()).isFalse();
|
||||
|
||||
context.close();
|
||||
assertThat(globalResources.isDisposed()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doNotStartBeforeApplicationContextFinish() {
|
||||
GenericApplicationContext context = new GenericApplicationContext() {
|
||||
@Override
|
||||
protected void finishRefresh() {
|
||||
}
|
||||
};
|
||||
context.registerBean(ReactorResourceFactory.class);
|
||||
context.refresh();
|
||||
|
||||
ReactorResourceFactory resourceFactory = context.getBeanFactory().getBean(ReactorResourceFactory.class);
|
||||
assertThat(resourceFactory.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazilyStartOnConnectionProviderAccess() {
|
||||
assertThat(this.resourceFactory.isRunning()).isFalse();
|
||||
this.resourceFactory.getConnectionProvider();
|
||||
assertThat(this.resourceFactory.isRunning()).isTrue();
|
||||
this.resourceFactory.stop();
|
||||
assertThat(this.resourceFactory.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazilyStartOnLoopResourcesAccess() {
|
||||
assertThat(this.resourceFactory.isRunning()).isFalse();
|
||||
this.resourceFactory.getLoopResources();
|
||||
assertThat(this.resourceFactory.isRunning()).isTrue();
|
||||
this.resourceFactory.stop();
|
||||
assertThat(this.resourceFactory.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
+21
-5
@@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.1
|
||||
*/
|
||||
class ReactorClientHttpConnectorTests {
|
||||
|
||||
@@ -37,7 +38,20 @@ class ReactorClientHttpConnectorTests {
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartWithHttpClient() {
|
||||
HttpClient httpClient = HttpClient.create();
|
||||
ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
}
|
||||
@@ -58,10 +72,12 @@ class ReactorClientHttpConnectorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void restartWithHttpClient() {
|
||||
HttpClient httpClient = HttpClient.create();
|
||||
ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
void lateStartWithExternalResourceFactory() {
|
||||
ReactorResourceFactory resourceFactory = new ReactorResourceFactory();
|
||||
Function<HttpClient, HttpClient> mapper = Function.identity();
|
||||
ReactorClientHttpConnector connector = new ReactorClientHttpConnector(resourceFactory, mapper);
|
||||
assertThat(connector.isRunning()).isFalse();
|
||||
resourceFactory.start();
|
||||
connector.start();
|
||||
assertThat(connector.isRunning()).isTrue();
|
||||
connector.stop();
|
||||
|
||||
@@ -63,6 +63,8 @@ class Jaxb2XmlEncoderTests extends AbstractEncoderTests<Jaxb2XmlEncoder> {
|
||||
assertThat(this.encoder.canEncode(forClass(TypePojo.class), MediaType.APPLICATION_XML)).isTrue();
|
||||
assertThat(this.encoder.canEncode(forClass(getClass()), MediaType.APPLICATION_XML)).isFalse();
|
||||
|
||||
assertThat(this.encoder.canEncode(forClass(JAXBElement.class), MediaType.APPLICATION_XML)).isTrue();
|
||||
|
||||
// SPR-15464
|
||||
assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse();
|
||||
}
|
||||
|
||||
+1
-2
@@ -28,7 +28,6 @@ import java.util.stream.Stream;
|
||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||
import io.undertow.util.HeaderMap;
|
||||
import org.apache.tomcat.util.http.MimeHeaders;
|
||||
import org.assertj.core.api.StringAssert;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
@@ -90,7 +89,7 @@ class HeadersAdaptersTests {
|
||||
headers.add("TestHeader", "first");
|
||||
headers.add("TestHeader", "second");
|
||||
assertThat(headers.getFirst("TestHeader")).isEqualTo("first");
|
||||
assertThat(headers.get("TestHeader"), StringAssert.class).element(0).isEqualTo("first");
|
||||
assertThat(headers.get("TestHeader")).first().isEqualTo("first");
|
||||
}
|
||||
|
||||
@ParameterizedHeadersTest
|
||||
|
||||
+1
-1
@@ -73,7 +73,6 @@ class RestClientObservationTests {
|
||||
|
||||
@BeforeEach
|
||||
void setupEach() {
|
||||
|
||||
this.client = RestClient.builder()
|
||||
.messageConverters(converters -> converters.add(0, this.converter))
|
||||
.requestFactory(this.requestFactory)
|
||||
@@ -267,6 +266,7 @@ class RestClientObservationTests {
|
||||
|
||||
|
||||
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
|
||||
TestObservationRegistryAssert.assertThat(this.observationRegistry).hasNumberOfObservationsWithNameEqualTo("http.client.requests",1);
|
||||
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasObservationWithNameEqualTo("http.client.requests").that();
|
||||
}
|
||||
|
||||
+16
-1
@@ -50,6 +50,11 @@ class ServerHttpObservationFilterTests {
|
||||
private final MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
|
||||
@Test
|
||||
void filterShouldNotProcessAsyncDispatch() {
|
||||
assertThat(this.filter.shouldNotFilterAsyncDispatch()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterShouldFillObservationContext() throws Exception {
|
||||
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
|
||||
@@ -60,7 +65,7 @@ class ServerHttpObservationFilterTests {
|
||||
assertThat(context.getCarrier()).isEqualTo(this.request);
|
||||
assertThat(context.getResponse()).isEqualTo(this.response);
|
||||
assertThat(context.getPathPattern()).isNull();
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,6 +114,16 @@ class ServerHttpObservationFilterTests {
|
||||
.hasLowCardinalityKeyValue("status", "500");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCloseObservationAfterAsyncCompletion() throws Exception {
|
||||
this.request.setAsyncSupported(true);
|
||||
this.request.startAsync();
|
||||
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
|
||||
this.request.getAsyncContext().complete();
|
||||
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
|
||||
}
|
||||
|
||||
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
|
||||
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasObservationWithNameEqualTo("http.server.requests").that();
|
||||
|
||||
@@ -40,14 +40,14 @@ class HandlerMethodTests {
|
||||
@Test
|
||||
void shouldValidateArgsWithConstraintsDirectlyOnClass() {
|
||||
Object target = new MyClass();
|
||||
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addNames"), true);
|
||||
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople", "addNames"), true);
|
||||
testValidateArgs(target, List.of("addPerson", "getPerson", "getIntValue", "addPersonNotValidated"), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldValidateArgsWithConstraintsOnInterface() {
|
||||
Object target = new MyInterfaceImpl();
|
||||
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons"), true);
|
||||
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople"), true);
|
||||
testValidateArgs(target, List.of("addPerson", "addPersonNotValidated", "getPerson", "getIntValue"), false);
|
||||
}
|
||||
|
||||
@@ -115,6 +115,9 @@ class HandlerMethodTests {
|
||||
public void addPersons(@Valid List<Person> persons) {
|
||||
}
|
||||
|
||||
public void addPeople(List<@Valid Person> persons) {
|
||||
}
|
||||
|
||||
public void addNames(List<@NotEmpty String> names) {
|
||||
}
|
||||
|
||||
@@ -144,6 +147,8 @@ class HandlerMethodTests {
|
||||
|
||||
void addPersons(@Valid List<Person> persons);
|
||||
|
||||
void addPeople(List<@Valid Person> persons);
|
||||
|
||||
void addPersonNotValidated(Person person);
|
||||
|
||||
@Valid
|
||||
@@ -173,6 +178,10 @@ class HandlerMethodTests {
|
||||
public void addPersons(List<Person> persons) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPeople(List<@Valid Person> persons) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPersonNotValidated(Person person) {
|
||||
}
|
||||
|
||||
+3
-4
@@ -27,7 +27,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.HttpSessionRequiredException;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||
@@ -43,7 +42,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@@ -151,7 +150,7 @@ class ModelFactoryTests {
|
||||
void sessionAttributeNotPresent() throws Exception {
|
||||
ModelFactory modelFactory = new ModelFactory(null, null, this.attributeHandler);
|
||||
HandlerMethod handlerMethod = createHandlerMethod("handleSessionAttr", String.class);
|
||||
assertThatExceptionOfType(HttpSessionRequiredException.class).isThrownBy(() ->
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod));
|
||||
|
||||
// Now add attribute and try again
|
||||
@@ -164,7 +163,7 @@ class ModelFactoryTests {
|
||||
void sessionAttributeByType() throws Exception {
|
||||
ModelFactory modelFactory = new ModelFactory(null, null, this.attributeHandler);
|
||||
HandlerMethod handlerMethod = createHandlerMethod("handleTestBean", TestBean.class);
|
||||
assertThatExceptionOfType(HttpSessionRequiredException.class).isThrownBy(() ->
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod));
|
||||
|
||||
// Now add attribute and try again
|
||||
|
||||
+16
@@ -17,12 +17,15 @@
|
||||
package org.springframework.web.util;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.FastByteArrayOutputStream;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -89,6 +92,19 @@ class ContentCachingRequestWrapperTests {
|
||||
assertThat(wrapper.getContentAsString()).isEqualTo(new String("Hel".getBytes(CHARSET), CHARSET));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotAllocateMoreThanCacheLimit() throws Exception {
|
||||
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(createGetRequest("Hello World"), CONTENT_CACHE_LIMIT);
|
||||
Field field = ReflectionUtils.findField(ContentCachingRequestWrapper.class, "cachedContent");
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
FastByteArrayOutputStream cachedContent = (FastByteArrayOutputStream) ReflectionUtils.getField(field, wrapper);
|
||||
field = ReflectionUtils.findField(FastByteArrayOutputStream.class, "initialBlockSize");
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
int blockSize = (int) ReflectionUtils.getField(field, cachedContent);
|
||||
assertThat(blockSize).isEqualTo(CONTENT_CACHE_LIMIT);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void cachedContentWithOverflow() throws Exception {
|
||||
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(
|
||||
|
||||
Reference in New Issue
Block a user