mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06b492dc0e | |||
| 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 | |||
| 89dd247b97 | |||
| 39dd1e4049 | |||
| 34f4ad3b71 | |||
| ea596aa211 | |||
| 58da30cd30 | |||
| cd33b4e35a | |||
| 20dea0dae2 | |||
| fee17e11ba | |||
| 65e1337d35 | |||
| a4c2f291d9 | |||
| f26483d272 | |||
| 481d036f7a | |||
| 2a2ef443a5 | |||
| 617833bec9 | |||
| 4d633c2ea8 |
@@ -51,3 +51,5 @@ atlassian-ide-plugin.xml
|
||||
.vscode/
|
||||
|
||||
cached-antora-playbook.yml
|
||||
|
||||
node_modules
|
||||
|
||||
@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
|
||||
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
|
||||
contacting a project maintainer at spring-code-of-conduct@spring.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.
|
||||
|
||||
+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
|
||||
|
||||
|
||||
@@ -89,8 +89,6 @@ configure([rootProject] + javaProjects) { project ->
|
||||
ext.javadocLinks = [
|
||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
"https://jakarta.ee/specifications/platform/9/apidocs/",
|
||||
"https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ and weblogic.* packages
|
||||
"https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/", // org.jboss.resource.*
|
||||
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
|
||||
"https://eclipse.dev/aspectj/doc/released/aspectj5rt-api",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
antora:
|
||||
extensions:
|
||||
- require: '@springio/antora-extensions'
|
||||
root_component_name: 'framework'
|
||||
site:
|
||||
title: Spring Framework
|
||||
url: https://docs.spring.io/spring-framework/reference
|
||||
robots: allow
|
||||
git:
|
||||
ensure_git_suffix: false
|
||||
content:
|
||||
sources:
|
||||
- url: https://github.com/spring-projects/spring-framework
|
||||
# Refname matching:
|
||||
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
|
||||
branches: ['main', '{6..9}.+({0..9}).x']
|
||||
tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})']
|
||||
start_path: framework-docs
|
||||
asciidoc:
|
||||
extensions:
|
||||
- '@asciidoctor/tabs'
|
||||
- '@springio/asciidoctor-extensions'
|
||||
- '@springio/asciidoctor-extensions/include-code-extension'
|
||||
attributes:
|
||||
page-stackoverflow-url: https://stackoverflow.com/questions/tagged/spring
|
||||
page-pagination: ''
|
||||
hide-uri-scheme: '@'
|
||||
tabs-sync-option: '@'
|
||||
include-java: 'example$docs-src/main/java/org/springframework/docs'
|
||||
urls:
|
||||
latest_version_segment_strategy: redirect:to
|
||||
latest_version_segment: ''
|
||||
redirect_facility: httpd
|
||||
runtime:
|
||||
log:
|
||||
failure_level: warn
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip
|
||||
@@ -10,27 +10,10 @@ apply from: "${rootDir}/gradle/ide.gradle"
|
||||
apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
antora {
|
||||
version = '3.2.0-alpha.2'
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
playbookProvider {
|
||||
repository = 'spring-projects/spring-framework'
|
||||
branch = 'docs-build'
|
||||
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
|
||||
checkLocalBranch = true
|
||||
}
|
||||
options = ['--clean', '--stacktrace']
|
||||
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
|
||||
environment = [
|
||||
'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691',
|
||||
'ALGOLIA_APP_ID': '244V8V9FGG',
|
||||
'ALGOLIA_INDEX_NAME': 'framework-docs'
|
||||
]
|
||||
dependencies = [
|
||||
'@antora/atlas-extension': '1.0.0-alpha.1',
|
||||
'@antora/collector-extension': '1.0.0-alpha.3',
|
||||
'@asciidoctor/tabs': '1.0.0-beta.3',
|
||||
'@opendevise/antora-release-line-extension': '1.0.0',
|
||||
'@springio/antora-extensions': '1.8.2',
|
||||
'@springio/asciidoctor-extensions': '1.0.0-alpha.9'
|
||||
'BUILD_REFNAME': 'HEAD',
|
||||
'BUILD_VERSION': project.version,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -24,7 +24,7 @@ To create the archive, two additional JVM flags must be specified:
|
||||
* `-Dspring.context.exit=onRefresh`: starts and then immediately exits your Spring
|
||||
application as described above
|
||||
|
||||
To create a CDS archive, your JDK must have a base image. If you add the flags above to
|
||||
To create a CDS archive, your JDK/JRE must have a base image. If you add the flags above to
|
||||
your startup script, you may get a warning that looks like this:
|
||||
|
||||
[source,shell,indent=0,subs="verbatim"]
|
||||
@@ -32,7 +32,8 @@ your startup script, you may get a warning that looks like this:
|
||||
-XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info.
|
||||
----
|
||||
|
||||
The base CDS archive can be created by issuing the following command:
|
||||
The base CDS archive is usually provided out-of-the-box, but can also be created if needed by issuing the following
|
||||
command:
|
||||
|
||||
[source,shell,indent=0,subs="verbatim"]
|
||||
----
|
||||
@@ -44,6 +45,9 @@ The base CDS archive can be created by issuing the following command:
|
||||
Once the archive is available, add `-XX:SharedArchiveFile=application.jsa` to your startup
|
||||
script to use it, assuming an `application.jsa` file in the working directory.
|
||||
|
||||
To check if the CDS cache is effective, you can use (for testing purposes only, not in production) `-Xshare:on` which
|
||||
prints an error message and exits if CDS can't be enabled.
|
||||
|
||||
To figure out how effective the cache is, you can enable class loading logs by adding
|
||||
an extra attribute: `-Xlog:class+load:file=cds.log`. This creates a `cds.log` with every
|
||||
attempt to load a class and its source. Classes that are loaded from the cache should have
|
||||
@@ -58,8 +62,11 @@ a "shared objects file" source, as shown in the following example:
|
||||
[0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top)
|
||||
----
|
||||
|
||||
TIP: If you have a large number of classes that are not loaded from the cache, make sure that
|
||||
the JDK and classpath used by the commands that create the archive and start the application
|
||||
are identical. Note also that to effectively cache classes, the classpath should be specified
|
||||
as a list of JARs containing those classes, and avoid the usage of directories and `*`
|
||||
wildcard characters.
|
||||
If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that
|
||||
the following conditions are fulfilled when creating and using the archive:
|
||||
|
||||
- The very same JVM must used.
|
||||
- The classpath must be specified as a list of JARs, and avoid the usage of directories and `*` wildcard characters.
|
||||
- The timestamps of the JARs must be preserved.
|
||||
- When using the archive, the classpath must be the same than the one used to create the archive, in the same order.
|
||||
Additional JARs or directories can be specified *at the end* (but won't be cached).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -3,28 +3,40 @@
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-validation.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] support for
|
||||
`@RequestMapping` methods, including the option to use
|
||||
xref:core/validation/beanvalidation.adoc[Java Bean Validation].
|
||||
The validation support works on two levels.
|
||||
Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] for
|
||||
`@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation].
|
||||
Validation may be applied at one of two levels:
|
||||
|
||||
First, resolvers for
|
||||
xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
|
||||
1. xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
|
||||
xref:web/webflux/controller/ann-methods/requestbody.adoc[@RequestBody], and
|
||||
xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] method
|
||||
parameters perform validation if the parameter has Jakarta's `@Valid` or Spring's
|
||||
`@Validated` annotation, and raise `MethodArgumentNotValidException` if necessary.
|
||||
Alternatively, you can handle the errors in the controller method by adding an
|
||||
`Errors` or `BindingResult` method parameter immediately after the validated one.
|
||||
xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] argument
|
||||
resolvers validate a method argument individually if the method parameter is annotated
|
||||
with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or
|
||||
`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be
|
||||
discussed next). The exception raised in this case is `MethodArgumentNotValidException`.
|
||||
|
||||
Second, if {bean-validation-site}[Java Bean Validation] is present _AND_ any method
|
||||
parameter has `@Constraint` annotations, then method validation is applied instead,
|
||||
raising `HandlerMethodValidationException` if necessary. For this case you can still add
|
||||
an `Errors` or `BindingResult` method parameter to handle validation errors within the
|
||||
controller method, but if other method arguments have validation errors then
|
||||
`HandlerMethodValidationException` is raised instead. Method validation can apply
|
||||
to the return value if the method is annotated with `@Valid` or with `@Constraint`
|
||||
annotations.
|
||||
2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared
|
||||
directly on method parameters, or on the method (for the return value), then method
|
||||
validation must be applied, and that supersedes validation at the method argument level
|
||||
because method validation covers both method parameter constraints and nested constraints
|
||||
via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`.
|
||||
|
||||
Applications must handle both `MethodArgumentNotValidException` and
|
||||
`HandlerMethodValidationException` as either may be raised depending on the controller
|
||||
method signature. The two exceptions, however are designed to be very similar, and can be
|
||||
handled with almost identical code. The main difference is that the former is for a single
|
||||
object while the latter is for a list of method parameters.
|
||||
|
||||
NOTE: `@Valid` is not a constraint annotation, but rather for nested constraints within
|
||||
an Object. Therefore, by itself `@Valid` does not lead to method validation. `@NotNull`
|
||||
on the other hand is a constraint, and adding it to an `@Valid` parameter leads to method
|
||||
validation. For nullability specifically, you may also use the `required` flag of
|
||||
`@RequestBody` or `@ModelAttribute`.
|
||||
|
||||
Method validation may be used in combination with `Errors` or `BindingResult` method
|
||||
parameters. However, the controller method is called only if all validation errors are on
|
||||
method parameters with an `Errors` immediately after. If there are validation errors on
|
||||
any other method parameter then `HandlerMethodValidationException` is raised.
|
||||
|
||||
You can configure a `Validator` globally through the
|
||||
xref:web/webflux/config.adoc#webflux-config-validation[WebMvc config], or locally
|
||||
|
||||
@@ -3,28 +3,40 @@
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-validation.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC has built-in xref:core/validation/validator.adoc[Validation] support for
|
||||
`@RequestMapping` methods, including the option to use
|
||||
xref:core/validation/beanvalidation.adoc[Java Bean Validation].
|
||||
The validation support works on two levels.
|
||||
Spring MVC has built-in xref:core/validation/validator.adoc[validation] for
|
||||
`@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation].
|
||||
Validation may be applied at one of two levels:
|
||||
|
||||
First, resolvers for
|
||||
xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
|
||||
1. xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
|
||||
xref:web/webmvc/mvc-controller/ann-methods/requestbody.adoc[@RequestBody], and
|
||||
xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] method
|
||||
parameters perform validation if the parameter has Jakarta's `@Valid` or Spring's
|
||||
`@Validated` annotation, and raise `MethodArgumentNotValidException` if necessary.
|
||||
Alternatively, you can handle the errors in the controller method by adding an
|
||||
`Errors` or `BindingResult` method parameter immediately after the validated one.
|
||||
xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] argument
|
||||
resolvers validate a method argument individually if the method parameter is annotated
|
||||
with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or
|
||||
`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be
|
||||
discussed next). The exception raised in this case is `MethodArgumentNotValidException`.
|
||||
|
||||
Second, if {bean-validation-site}[Java Bean Validation] is present _AND_ any method
|
||||
parameter has `@Constraint` annotations, then method validation is applied instead,
|
||||
raising `HandlerMethodValidationException` if necessary. For this case you can still add
|
||||
an `Errors` or `BindingResult` method parameter to handle validation errors within the
|
||||
controller method, but if other method arguments have validation errors then
|
||||
`HandlerMethodValidationException` is raised instead. Method validation can apply
|
||||
to the return value if the method is annotated with `@Valid` or with `@Constraint`
|
||||
annotations.
|
||||
2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared
|
||||
directly on method parameters, or on the method (for the return value), then method
|
||||
validation must be applied, and that supersedes validation at the method argument level
|
||||
because method validation covers both method parameter constraints and nested constraints
|
||||
via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`.
|
||||
|
||||
Applications must handle both `MethodArgumentNotValidException` and
|
||||
`HandlerMethodValidationException` as either may be raised depending on the controller
|
||||
method signature. The two exceptions, however are designed to be very similar, and can be
|
||||
handled with almost identical code. The main difference is that the former is for a single
|
||||
object while the latter is for a list of method parameters.
|
||||
|
||||
NOTE: `@Valid` is not a constraint annotation, but rather for nested constraints within
|
||||
an Object. Therefore, by itself `@Valid` does not lead to method validation. `@NotNull`
|
||||
on the other hand is a constraint, and adding it to an `@Valid` parameter leads to method
|
||||
validation. For nullability specifically, you may also use the `required` flag of
|
||||
`@RequestBody` or `@ModelAttribute`.
|
||||
|
||||
Method validation may be used in combination with `Errors` or `BindingResult` method
|
||||
parameters. However, the controller method is called only if all validation errors are on
|
||||
method parameters with an `Errors` immediately after. If there are validation errors on
|
||||
any other method parameter then `HandlerMethodValidationException` is raised.
|
||||
|
||||
You can configure a `Validator` globally through the
|
||||
xref:web/webmvc/mvc-config/validation.adoc[WebMvc config], or locally through an
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.4",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.11.1",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
}
|
||||
}
|
||||
@@ -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,12 +110,12 @@ 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")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.3")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.4")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -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.7-SNAPSHOT
|
||||
version=6.1.9
|
||||
|
||||
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
|
||||
|
||||
+33
-3
@@ -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;
|
||||
@@ -41,6 +42,7 @@ import org.aspectj.weaver.tools.PointcutParameter;
|
||||
import org.aspectj.weaver.tools.PointcutParser;
|
||||
import org.aspectj.weaver.tools.PointcutPrimitive;
|
||||
import org.aspectj.weaver.tools.ShadowMatch;
|
||||
import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException;
|
||||
|
||||
import org.springframework.aop.ClassFilter;
|
||||
import org.springframework.aop.IntroductionAwareMethodMatcher;
|
||||
@@ -85,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,
|
||||
@@ -102,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];
|
||||
@@ -115,6 +121,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
@Nullable
|
||||
private transient PointcutExpression pointcutExpression;
|
||||
|
||||
private transient boolean pointcutParsingFailed = false;
|
||||
|
||||
private transient Map<Method, ShadowMatch> shadowMatchCache = new ConcurrentHashMap<>(32);
|
||||
|
||||
|
||||
@@ -131,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");
|
||||
@@ -146,6 +154,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
*/
|
||||
public void setPointcutDeclarationScope(Class<?> pointcutDeclarationScope) {
|
||||
this.pointcutDeclarationScope = pointcutDeclarationScope;
|
||||
this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,6 +279,15 @@ 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;
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
return obtainPointcutExpression().couldMatchJoinPointsInType(targetClass);
|
||||
@@ -283,8 +301,11 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException | IllegalStateException ex) {
|
||||
throw ex;
|
||||
catch (IllegalArgumentException | IllegalStateException | UnsupportedPointcutPrimitiveException ex) {
|
||||
this.pointcutParsingFailed = true;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Pointcut parser rejected expression [" + getExpression() + "]: " + ex);
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.debug("PointcutExpression matching rejected target class", ex);
|
||||
@@ -526,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-14
@@ -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.
|
||||
@@ -50,6 +50,7 @@ import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConvertingComparator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -133,17 +134,19 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
|
||||
|
||||
List<Advisor> advisors = new ArrayList<>();
|
||||
for (Method method : getAdvisorMethods(aspectClass)) {
|
||||
// Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect
|
||||
// to getAdvisor(...) to represent the "current position" in the declared methods list.
|
||||
// However, since Java 7 the "current position" is not valid since the JDK no longer
|
||||
// returns declared methods in the order in which they are declared in the source code.
|
||||
// Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods
|
||||
// discovered via reflection in order to support reliable advice ordering across JVM launches.
|
||||
// Specifically, a value of 0 aligns with the default value used in
|
||||
// AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor).
|
||||
Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName);
|
||||
if (advisor != null) {
|
||||
advisors.add(advisor);
|
||||
if (method.equals(ClassUtils.getMostSpecificMethod(method, aspectClass))) {
|
||||
// Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect
|
||||
// to getAdvisor(...) to represent the "current position" in the declared methods list.
|
||||
// However, since Java 7 the "current position" is not valid since the JDK no longer
|
||||
// returns declared methods in the order in which they are declared in the source code.
|
||||
// Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods
|
||||
// discovered via reflection in order to support reliable advice ordering across JVM launches.
|
||||
// Specifically, a value of 0 aligns with the default value used in
|
||||
// AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor).
|
||||
Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName);
|
||||
if (advisor != null) {
|
||||
advisors.add(advisor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -39,13 +39,13 @@ import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.beans.testfixture.beans.subpkg.DeepBean;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* @author Rob Harrop
|
||||
* @author Rod Johnson
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
* @author Yanming Zhou
|
||||
*/
|
||||
class AspectJExpressionPointcutTests {
|
||||
@@ -243,7 +243,7 @@ class AspectJExpressionPointcutTests {
|
||||
@Test
|
||||
void testInvalidExpression() {
|
||||
String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)";
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> getPointcut(expression).getClassFilter().matches(Object.class));
|
||||
assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse();
|
||||
}
|
||||
|
||||
private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) {
|
||||
|
||||
+10
-5
@@ -83,15 +83,15 @@ abstract class AbstractAspectJAdvisorFactoryTests {
|
||||
@Test
|
||||
void rejectsPerCflowAspect() {
|
||||
assertThatExceptionOfType(AopConfigException.class)
|
||||
.isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean")))
|
||||
.withMessageContaining("PERCFLOW");
|
||||
.isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean")))
|
||||
.withMessageContaining("PERCFLOW");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPerCflowBelowAspect() {
|
||||
assertThatExceptionOfType(AopConfigException.class)
|
||||
.isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean")))
|
||||
.withMessageContaining("PERCFLOWBELOW");
|
||||
.isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean")))
|
||||
.withMessageContaining("PERCFLOWBELOW");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -770,9 +770,15 @@ abstract class AbstractAspectJAdvisorFactoryTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Aspect
|
||||
static class IncrementingAspect extends DoublingAspect {
|
||||
|
||||
@Override
|
||||
public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable {
|
||||
return ((int) pjp.proceed()) * 2;
|
||||
}
|
||||
|
||||
@Around("execution(* getAge())")
|
||||
public int incrementAge(ProceedingJoinPoint pjp) throws Throwable {
|
||||
return ((int) pjp.proceed()) + 1;
|
||||
@@ -780,7 +786,6 @@ abstract class AbstractAspectJAdvisorFactoryTests {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Aspect
|
||||
private static class InvocationTrackingAspect {
|
||||
|
||||
|
||||
+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);
|
||||
|
||||
+2
-1
@@ -22,11 +22,12 @@ import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
|
||||
/**
|
||||
* @author Adrian Colyer
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class AutoProxyWithCodeStyleAspectsTests {
|
||||
|
||||
@Test
|
||||
void noAutoproxyingOfAjcCompiledAspects() {
|
||||
void noAutoProxyingOfAjcCompiledAspects() {
|
||||
new ClassPathXmlApplicationContext("org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml");
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -20,11 +20,14 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
|
||||
public class SpringConfiguredWithAutoProxyingTests {
|
||||
/**
|
||||
* @author Ramnivas Laddad
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class SpringConfiguredWithAutoProxyingTests {
|
||||
|
||||
@Test
|
||||
void springConfiguredAndAutoProxyUsedTogether() {
|
||||
// instantiation is sufficient to trigger failure if this is going to fail...
|
||||
new ClassPathXmlApplicationContext("org/springframework/beans/factory/aspectj/springConfigured.xml");
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -2,16 +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/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/>
|
||||
|
||||
<bean id="myAspect" class="org.springframework.aop.aspectj.autoproxy.CodeStyleAspect"
|
||||
factory-method="aspectOf">
|
||||
<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="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>
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ class BeanDefinitionMethodGenerator {
|
||||
this.aotContributions.forEach(aotContribution -> aotContribution.applyTo(generationContext, codeGenerator));
|
||||
|
||||
CodeWarnings codeWarnings = new CodeWarnings();
|
||||
codeWarnings.detectDeprecation(this.registeredBean.getBeanClass());
|
||||
codeWarnings.detectDeprecation(this.registeredBean.getBeanType());
|
||||
return generatedMethods.add("getBeanDefinition", method -> {
|
||||
method.addJavadoc("Get the $L definition for '$L'.",
|
||||
(this.registeredBean.isInnerBean() ? "inner-bean" : "bean"),
|
||||
|
||||
@@ -23,10 +23,12 @@ import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.javapoet.AnnotationSpec;
|
||||
import org.springframework.javapoet.CodeBlock;
|
||||
import org.springframework.javapoet.MethodSpec;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Helper class to register warnings that the compiler may trigger on
|
||||
@@ -72,6 +74,26 @@ class CodeWarnings {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the presence of {@link Deprecated} on the signature of the
|
||||
* specified {@link ResolvableType}.
|
||||
* @param resolvableType a type signature
|
||||
* @return {@code this} instance
|
||||
*/
|
||||
public CodeWarnings detectDeprecation(ResolvableType resolvableType) {
|
||||
if (ResolvableType.NONE.equals(resolvableType)) {
|
||||
return this;
|
||||
}
|
||||
Class<?> type = ClassUtils.getUserClass(resolvableType.toClass());
|
||||
detectDeprecation(type);
|
||||
if (resolvableType.hasGenerics() && !resolvableType.hasUnresolvableGenerics()) {
|
||||
for (ResolvableType generic : resolvableType.getGenerics()) {
|
||||
detectDeprecation(generic);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include {@link SuppressWarnings} on the specified method if necessary.
|
||||
* @param method the method to update
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+13
@@ -782,6 +782,19 @@ class BeanDefinitionMethodGeneratorTests {
|
||||
compileAndCheckWarnings(method);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateBeanDefinitionMethodWithDeprecatedGenericElementInTargetClass() {
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition();
|
||||
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(GenericBean.class, DeprecatedBean.class));
|
||||
RegisteredBean registeredBean = registerBean(beanDefinition);
|
||||
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
|
||||
methodGeneratorFactory, registeredBean, null,
|
||||
Collections.emptyList());
|
||||
MethodReference method = generator.generateBeanDefinitionMethod(
|
||||
generationContext, beanRegistrationsCode);
|
||||
compileAndCheckWarnings(method);
|
||||
}
|
||||
|
||||
private void compileAndCheckWarnings(MethodReference methodReference) {
|
||||
assertThatNoException().isThrownBy(() -> compile(TEST_COMPILER, methodReference,
|
||||
((instanceSupplier, compiled) -> {})));
|
||||
|
||||
+40
@@ -17,15 +17,21 @@
|
||||
package org.springframework.beans.factory.aot;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.aot.test.generate.TestGenerationContext;
|
||||
import org.springframework.beans.testfixture.beans.GenericBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder;
|
||||
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.generator.deprecation.DeprecatedForRemovalBean;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.test.tools.Compiled;
|
||||
import org.springframework.core.test.tools.TestCompiler;
|
||||
import org.springframework.javapoet.MethodSpec;
|
||||
@@ -98,6 +104,40 @@ class CodeWarningsTests {
|
||||
assertThat(this.codeWarnings.getWarnings()).containsExactly("removal");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("resolvableTypesWithDeprecated")
|
||||
void detectDeprecationOnResolvableTypeWithDeprecated(ResolvableType resolvableType) {
|
||||
this.codeWarnings.detectDeprecation(resolvableType);
|
||||
assertThat(this.codeWarnings.getWarnings()).containsExactly("deprecation");
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
static Stream<Arguments> resolvableTypesWithDeprecated() {
|
||||
return Stream.of(
|
||||
Arguments.of(ResolvableType.forClass(DeprecatedBean.class)),
|
||||
Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, DeprecatedBean.class)),
|
||||
Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class,
|
||||
ResolvableType.forClassWithGenerics(GenericBean.class, DeprecatedBean.class)))
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("resolvableTypesWithDeprecatedForRemoval")
|
||||
void detectDeprecationOnResolvableTypeWithDeprecatedForRemoval(ResolvableType resolvableType) {
|
||||
this.codeWarnings.detectDeprecation(resolvableType);
|
||||
assertThat(this.codeWarnings.getWarnings()).containsExactly("removal");
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
static Stream<Arguments> resolvableTypesWithDeprecatedForRemoval() {
|
||||
return Stream.of(
|
||||
Arguments.of(ResolvableType.forClass(DeprecatedForRemovalBean.class)),
|
||||
Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class, DeprecatedForRemovalBean.class)),
|
||||
Arguments.of(ResolvableType.forClassWithGenerics(GenericBean.class,
|
||||
ResolvableType.forClassWithGenerics(GenericBean.class, DeprecatedForRemovalBean.class)))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringIncludeWarnings() {
|
||||
this.codeWarnings.register("deprecation");
|
||||
|
||||
+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
-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);
|
||||
}
|
||||
|
||||
+28
-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.
|
||||
@@ -22,8 +22,10 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.springframework.format.Formatter;
|
||||
@@ -35,9 +37,14 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A formatter for {@link java.util.Date} types.
|
||||
*
|
||||
* <p>Supports the configuration of an explicit date time pattern, timezone,
|
||||
* locale, and fallback date time patterns for lenient parsing.
|
||||
*
|
||||
* <p>Common ISO patterns for UTC instants are applied at millisecond precision.
|
||||
* Note that {@link org.springframework.format.datetime.standard.InstantFormatter}
|
||||
* is recommended for flexible UTC parsing into a {@link java.time.Instant} instead.
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Juergen Hoeller
|
||||
* @author Phillip Webb
|
||||
@@ -49,15 +56,23 @@ public class DateFormatter implements Formatter<Date> {
|
||||
|
||||
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
|
||||
|
||||
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
|
||||
private static final Map<ISO, String> ISO_PATTERNS;
|
||||
|
||||
private static final Map<ISO, String> ISO_FALLBACK_PATTERNS;
|
||||
|
||||
static {
|
||||
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
|
||||
Map<ISO, String> formats = new EnumMap<>(ISO.class);
|
||||
formats.put(ISO.DATE, "yyyy-MM-dd");
|
||||
formats.put(ISO.TIME, "HH:mm:ss.SSSXXX");
|
||||
formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
|
||||
ISO_PATTERNS = Collections.unmodifiableMap(formats);
|
||||
|
||||
// Fallback format for the time part without milliseconds.
|
||||
Map<ISO, String> fallbackFormats = new EnumMap<>(ISO.class);
|
||||
fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX");
|
||||
fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
||||
ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,8 +217,16 @@ public class DateFormatter implements Formatter<Date> {
|
||||
return getDateFormat(locale).parse(text);
|
||||
}
|
||||
catch (ParseException ex) {
|
||||
Set<String> fallbackPatterns = new LinkedHashSet<>();
|
||||
String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso);
|
||||
if (isoPattern != null) {
|
||||
fallbackPatterns.add(isoPattern);
|
||||
}
|
||||
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
|
||||
for (String pattern : this.fallbackPatterns) {
|
||||
Collections.addAll(fallbackPatterns, this.fallbackPatterns);
|
||||
}
|
||||
if (!fallbackPatterns.isEmpty()) {
|
||||
for (String pattern : fallbackPatterns) {
|
||||
try {
|
||||
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
|
||||
// Align timezone for parsing format with printing format if ISO is set.
|
||||
@@ -221,8 +244,8 @@ public class DateFormatter implements Formatter<Date> {
|
||||
}
|
||||
if (this.source != null) {
|
||||
ParseException parseException = new ParseException(
|
||||
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
|
||||
ex.getErrorOffset());
|
||||
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
|
||||
ex.getErrorOffset());
|
||||
parseException.initCause(ex);
|
||||
throw parseException;
|
||||
}
|
||||
|
||||
+3
-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.
|
||||
@@ -45,12 +45,12 @@ public class InstantFormatter implements Formatter<Instant> {
|
||||
return Instant.ofEpochMilli(Long.parseLong(text));
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) {
|
||||
if (!text.isEmpty() && Character.isAlphabetic(text.charAt(0))) {
|
||||
// assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT"
|
||||
return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text));
|
||||
}
|
||||
else {
|
||||
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
|
||||
// assuming UTC instant a la "2007-12-03T10:15:30.000Z"
|
||||
return Instant.parse(text);
|
||||
}
|
||||
}
|
||||
|
||||
+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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -28,17 +28,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*
|
||||
* @author Adrian Colyer
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class OverloadedAdviceTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
void testExceptionOnConfigParsingWithMismatchedAdviceMethod() {
|
||||
assertThatExceptionOfType(BeanCreationException.class)
|
||||
.isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()))
|
||||
.havingRootCause()
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.as("invalidAbsoluteTypeName should be detected by AJ").withMessageContaining("invalidAbsoluteTypeName");
|
||||
void testConfigParsingWithMismatchedAdviceMethod() {
|
||||
new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+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 {
|
||||
|
||||
|
||||
+26
-7
@@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Phillip Webb
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class DateFormatterTests {
|
||||
|
||||
@@ -45,6 +46,7 @@ class DateFormatterTests {
|
||||
void shouldPrintAndParseDefault() throws Exception {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
|
||||
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
|
||||
@@ -54,6 +56,7 @@ class DateFormatterTests {
|
||||
void shouldPrintAndParseFromPattern() throws ParseException {
|
||||
DateFormatter formatter = new DateFormatter("yyyy-MM-dd");
|
||||
formatter.setTimeZone(UTC);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
|
||||
assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date);
|
||||
@@ -64,6 +67,7 @@ class DateFormatterTests {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setStyle(DateFormat.SHORT);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09");
|
||||
assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date);
|
||||
@@ -74,6 +78,7 @@ class DateFormatterTests {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setStyle(DateFormat.MEDIUM);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
|
||||
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
|
||||
@@ -84,6 +89,7 @@ class DateFormatterTests {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setStyle(DateFormat.LONG);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009");
|
||||
assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date);
|
||||
@@ -94,16 +100,18 @@ class DateFormatterTests {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setStyle(DateFormat.FULL);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009");
|
||||
assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintAndParseISODate() throws Exception {
|
||||
void shouldPrintAndParseIsoDate() throws Exception {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setIso(ISO.DATE);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
|
||||
assertThat(formatter.parse("2009-6-01", Locale.US))
|
||||
@@ -111,33 +119,44 @@ class DateFormatterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintAndParseISOTime() throws Exception {
|
||||
void shouldPrintAndParseIsoTime() throws Exception {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setIso(ISO.TIME);
|
||||
|
||||
Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z");
|
||||
assertThat(formatter.parse("14:23:05.003Z", Locale.US))
|
||||
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3));
|
||||
|
||||
date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 0);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.000Z");
|
||||
assertThat(formatter.parse("14:23:05Z", Locale.US))
|
||||
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 0).toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintAndParseISODateTime() throws Exception {
|
||||
void shouldPrintAndParseIsoDateTime() throws Exception {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setIso(ISO.DATE_TIME);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z");
|
||||
assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date);
|
||||
|
||||
date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 0);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.000Z");
|
||||
assertThat(formatter.parse("2009-06-01T14:23:05Z", Locale.US)).isEqualTo(date.toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowOnUnsupportedStylePattern() {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setStylePattern("OO");
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
formatter.parse("2009", Locale.US))
|
||||
.withMessageContaining("Unsupported style pattern 'OO'");
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US))
|
||||
.withMessageContaining("Unsupported style pattern 'OO'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -148,8 +167,8 @@ class DateFormatterTests {
|
||||
formatter.setStylePattern("L-");
|
||||
formatter.setIso(ISO.DATE_TIME);
|
||||
formatter.setPattern("yyyy");
|
||||
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
|
||||
assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009");
|
||||
|
||||
formatter.setPattern("");
|
||||
|
||||
+10
-20
@@ -274,9 +274,7 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
|
||||
assertThat(value)
|
||||
.startsWith("10/31/09")
|
||||
.endsWith("12:00 PM");
|
||||
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -286,9 +284,7 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
|
||||
assertThat(value)
|
||||
.startsWith("10/31/09")
|
||||
.endsWith("12:00 PM");
|
||||
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -298,9 +294,7 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("styleLocalDateTime").toString();
|
||||
assertThat(value)
|
||||
.startsWith("Oct 31, 2009")
|
||||
.endsWith("12:00:00 PM");
|
||||
assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -310,9 +304,7 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
|
||||
assertThat(value)
|
||||
.startsWith("10/31/09")
|
||||
.endsWith("12:00 PM");
|
||||
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -325,9 +317,7 @@ class DateTimeFormattingTests {
|
||||
binder.bind(propertyValues);
|
||||
assertThat(binder.getBindingResult().getErrorCount()).isZero();
|
||||
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
|
||||
assertThat(value)
|
||||
.startsWith("Oct 31, 2009")
|
||||
.endsWith("12:00:00 PM");
|
||||
assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -540,6 +530,7 @@ class DateTimeFormattingTests {
|
||||
assertThat(binder.getBindingResult().getRawFieldValue("monthDayAnnotatedPattern")).isEqualTo(MonthDay.parse("--01-03"));
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class FallbackPatternTests {
|
||||
|
||||
@@ -644,10 +635,10 @@ class DateTimeFormattingTests {
|
||||
@DateTimeFormat(style = "M-")
|
||||
private LocalDate styleLocalDate;
|
||||
|
||||
@DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
|
||||
@DateTimeFormat(style = "S-", fallbackPatterns = {"yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd"})
|
||||
private LocalDate styleLocalDateWithFallbackPatterns;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = {"M/d/yy", "yyyyMMdd", "yyyy.MM.dd"})
|
||||
private LocalDate patternLocalDateWithFallbackPatterns;
|
||||
|
||||
private LocalTime localTime;
|
||||
@@ -655,7 +646,7 @@ class DateTimeFormattingTests {
|
||||
@DateTimeFormat(style = "-M")
|
||||
private LocalTime styleLocalTime;
|
||||
|
||||
@DateTimeFormat(style = "-M", fallbackPatterns = { "HH:mm:ss", "HH:mm"})
|
||||
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"})
|
||||
private LocalTime styleLocalTimeWithFallbackPatterns;
|
||||
|
||||
private LocalDateTime localDateTime;
|
||||
@@ -675,7 +666,7 @@ class DateTimeFormattingTests {
|
||||
@DateTimeFormat(iso = ISO.DATE_TIME)
|
||||
private LocalDateTime isoLocalDateTime;
|
||||
|
||||
@DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = { "yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"})
|
||||
@DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = {"yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"})
|
||||
private LocalDateTime isoLocalDateTimeWithFallbackPatterns;
|
||||
|
||||
private Instant instant;
|
||||
@@ -703,7 +694,6 @@ class DateTimeFormattingTests {
|
||||
|
||||
private final List<DateTimeBean> children = new ArrayList<>();
|
||||
|
||||
|
||||
public LocalDate getLocalDate() {
|
||||
return this.localDate;
|
||||
}
|
||||
|
||||
+11
-11
@@ -20,6 +20,7 @@ import java.text.ParseException;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -50,13 +51,12 @@ class InstantFormatterTests {
|
||||
|
||||
private final InstantFormatter instantFormatter = new InstantFormatter();
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(ISOSerializedInstantProvider.class)
|
||||
void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException {
|
||||
Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from);
|
||||
|
||||
Instant actual = instantFormatter.parse(input, null);
|
||||
|
||||
Instant actual = instantFormatter.parse(input, Locale.US);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@@ -64,9 +64,7 @@ class InstantFormatterTests {
|
||||
@ArgumentsSource(RFC1123SerializedInstantProvider.class)
|
||||
void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException {
|
||||
Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from);
|
||||
|
||||
Instant actual = instantFormatter.parse(input, null);
|
||||
|
||||
Instant actual = instantFormatter.parse(input, Locale.US);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@@ -74,20 +72,18 @@ class InstantFormatterTests {
|
||||
@ArgumentsSource(RandomInstantProvider.class)
|
||||
void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) {
|
||||
String expected = DateTimeFormatter.ISO_INSTANT.format(input);
|
||||
|
||||
String actual = instantFormatter.print(input, null);
|
||||
|
||||
String actual = instantFormatter.print(input, Locale.US);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ArgumentsSource(RandomEpochMillisProvider.class)
|
||||
void should_parse_into_an_Instant_from_epoch_milli(Instant input) throws ParseException {
|
||||
Instant actual = instantFormatter.parse(Long.toString(input.toEpochMilli()), null);
|
||||
|
||||
Instant actual = instantFormatter.parse(Long.toString(input.toEpochMilli()), Locale.US);
|
||||
assertThat(actual).isEqualTo(input);
|
||||
}
|
||||
|
||||
|
||||
private static class RandomInstantProvider implements ArgumentsProvider {
|
||||
|
||||
private static final long DATA_SET_SIZE = 10;
|
||||
@@ -109,6 +105,7 @@ class InstantFormatterTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class ISOSerializedInstantProvider extends RandomInstantProvider {
|
||||
|
||||
@Override
|
||||
@@ -117,6 +114,7 @@ class InstantFormatterTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class RFC1123SerializedInstantProvider extends RandomInstantProvider {
|
||||
|
||||
// RFC-1123 supports only 4-digit years
|
||||
@@ -130,6 +128,8 @@ class InstantFormatterTests {
|
||||
.map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final class RandomEpochMillisProvider implements ArgumentsProvider {
|
||||
|
||||
private static final long DATA_SET_SIZE = 10;
|
||||
|
||||
+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 {
|
||||
|
||||
+2
@@ -18,4 +18,6 @@
|
||||
|
||||
<bean id="testBean" class="org.springframework.beans.testfixture.beans.TestBean"/>
|
||||
|
||||
<bean id="testBean2" class="org.springframework.beans.testfixture.beans.TestBean"/>
|
||||
|
||||
</beans>
|
||||
+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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-12
@@ -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.
|
||||
@@ -154,7 +154,7 @@ final class PersistenceUnitReader {
|
||||
/**
|
||||
* Validate the given stream and return a valid DOM document for parsing.
|
||||
*/
|
||||
protected Document buildDocument(ErrorHandler handler, InputStream stream)
|
||||
Document buildDocument(ErrorHandler handler, InputStream stream)
|
||||
throws ParserConfigurationException, SAXException, IOException {
|
||||
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
@@ -168,9 +168,7 @@ final class PersistenceUnitReader {
|
||||
/**
|
||||
* Parse the validated document and add entries to the given unit info list.
|
||||
*/
|
||||
protected List<SpringPersistenceUnitInfo> parseDocument(
|
||||
Resource resource, Document document, List<SpringPersistenceUnitInfo> infos) throws IOException {
|
||||
|
||||
void parseDocument(Resource resource, Document document, List<SpringPersistenceUnitInfo> infos) throws IOException {
|
||||
Element persistence = document.getDocumentElement();
|
||||
String version = persistence.getAttribute(PERSISTENCE_VERSION);
|
||||
URL rootUrl = determinePersistenceUnitRootUrl(resource);
|
||||
@@ -179,14 +177,12 @@ final class PersistenceUnitReader {
|
||||
for (Element unit : units) {
|
||||
infos.add(parsePersistenceUnitInfo(unit, version, rootUrl));
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the unit info DOM element.
|
||||
*/
|
||||
protected SpringPersistenceUnitInfo parsePersistenceUnitInfo(
|
||||
SpringPersistenceUnitInfo parsePersistenceUnitInfo(
|
||||
Element persistenceUnit, String version, @Nullable URL rootUrl) throws IOException {
|
||||
|
||||
SpringPersistenceUnitInfo unitInfo = new SpringPersistenceUnitInfo();
|
||||
@@ -253,7 +249,7 @@ final class PersistenceUnitReader {
|
||||
/**
|
||||
* Parse the {@code property} XML elements.
|
||||
*/
|
||||
protected void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
|
||||
void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
|
||||
Element propRoot = DomUtils.getChildElementByTagName(persistenceUnit, PROPERTIES);
|
||||
if (propRoot == null) {
|
||||
return;
|
||||
@@ -269,7 +265,7 @@ final class PersistenceUnitReader {
|
||||
/**
|
||||
* Parse the {@code class} XML elements.
|
||||
*/
|
||||
protected void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
|
||||
void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
|
||||
List<Element> classes = DomUtils.getChildElementsByTagName(persistenceUnit, MANAGED_CLASS_NAME);
|
||||
for (Element element : classes) {
|
||||
String value = DomUtils.getTextValue(element).trim();
|
||||
@@ -282,7 +278,7 @@ final class PersistenceUnitReader {
|
||||
/**
|
||||
* Parse the {@code mapping-file} XML elements.
|
||||
*/
|
||||
protected void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
|
||||
void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
|
||||
List<Element> files = DomUtils.getChildElementsByTagName(persistenceUnit, MAPPING_FILE_NAME);
|
||||
for (Element element : files) {
|
||||
String value = DomUtils.getTextValue(element).trim();
|
||||
@@ -295,7 +291,7 @@ final class PersistenceUnitReader {
|
||||
/**
|
||||
* Parse the {@code jar-file} XML elements.
|
||||
*/
|
||||
protected void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException {
|
||||
void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException {
|
||||
List<Element> jars = DomUtils.getChildElementsByTagName(persistenceUnit, JAR_FILE_URL);
|
||||
for (Element element : jars) {
|
||||
String value = DomUtils.getTextValue(element).trim();
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.datatype.Duration;
|
||||
@@ -192,7 +194,7 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
|
||||
@Nullable
|
||||
private ClassLoader beanClassLoader;
|
||||
|
||||
private final Object jaxbContextMonitor = new Object();
|
||||
private final Lock jaxbContextLock = new ReentrantLock();
|
||||
|
||||
@Nullable
|
||||
private volatile JAXBContext jaxbContext;
|
||||
@@ -204,6 +206,12 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
|
||||
|
||||
private boolean processExternalEntities = false;
|
||||
|
||||
@Nullable
|
||||
private volatile SAXParserFactory schemaParserFactory;
|
||||
|
||||
@Nullable
|
||||
private volatile SAXParserFactory sourceParserFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Set multiple JAXB context paths. The given array of context paths gets
|
||||
@@ -426,6 +434,7 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
|
||||
*/
|
||||
public void setSupportDtd(boolean supportDtd) {
|
||||
this.supportDtd = supportDtd;
|
||||
this.sourceParserFactory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -450,6 +459,7 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
|
||||
if (processExternalEntities) {
|
||||
this.supportDtd = true;
|
||||
}
|
||||
this.sourceParserFactory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,7 +507,9 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
|
||||
if (context != null) {
|
||||
return context;
|
||||
}
|
||||
synchronized (this.jaxbContextMonitor) {
|
||||
|
||||
this.jaxbContextLock.lock();
|
||||
try {
|
||||
context = this.jaxbContext;
|
||||
if (context == null) {
|
||||
try {
|
||||
@@ -521,6 +533,9 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
|
||||
}
|
||||
return context;
|
||||
}
|
||||
finally {
|
||||
this.jaxbContextLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private JAXBContext createJaxbContextFromContextPath(String contextPath) throws JAXBException {
|
||||
@@ -587,17 +602,24 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
|
||||
Assert.notEmpty(resources, "No resources given");
|
||||
Assert.hasLength(schemaLanguage, "No schema language provided");
|
||||
Source[] schemaSources = new Source[resources.length];
|
||||
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
|
||||
|
||||
SAXParserFactory saxParserFactory = this.schemaParserFactory;
|
||||
if (saxParserFactory == null) {
|
||||
saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
|
||||
this.schemaParserFactory = saxParserFactory;
|
||||
}
|
||||
SAXParser saxParser = saxParserFactory.newSAXParser();
|
||||
XMLReader xmlReader = saxParser.getXMLReader();
|
||||
|
||||
for (int i = 0; i < resources.length; i++) {
|
||||
Resource resource = resources[i];
|
||||
Assert.isTrue(resource != null && resource.exists(), () -> "Resource does not exist: " + resource);
|
||||
InputSource inputSource = SaxResourceUtils.createInputSource(resource);
|
||||
schemaSources[i] = new SAXSource(xmlReader, inputSource);
|
||||
}
|
||||
|
||||
SchemaFactory schemaFactory = SchemaFactory.newInstance(schemaLanguage);
|
||||
if (this.schemaResourceResolver != null) {
|
||||
schemaFactory.setResourceResolver(this.schemaResourceResolver);
|
||||
@@ -886,11 +908,16 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
|
||||
|
||||
try {
|
||||
if (xmlReader == null) {
|
||||
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
String name = "http://xml.org/sax/features/external-general-entities";
|
||||
saxParserFactory.setFeature(name, isProcessExternalEntities());
|
||||
SAXParserFactory saxParserFactory = this.sourceParserFactory;
|
||||
if (saxParserFactory == null) {
|
||||
saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setFeature(
|
||||
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
saxParserFactory.setFeature(
|
||||
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
|
||||
this.sourceParserFactory = saxParserFactory;
|
||||
}
|
||||
SAXParser saxParser = saxParserFactory.newSAXParser();
|
||||
xmlReader = saxParser.getXMLReader();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -83,9 +83,10 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
|
||||
private boolean processExternalEntities = false;
|
||||
|
||||
@Nullable
|
||||
private DocumentBuilderFactory documentBuilderFactory;
|
||||
private volatile DocumentBuilderFactory documentBuilderFactory;
|
||||
|
||||
private final Object documentBuilderFactoryMonitor = new Object();
|
||||
@Nullable
|
||||
private volatile SAXParserFactory saxParserFactory;
|
||||
|
||||
|
||||
/**
|
||||
@@ -94,6 +95,8 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
|
||||
*/
|
||||
public void setSupportDtd(boolean supportDtd) {
|
||||
this.supportDtd = supportDtd;
|
||||
this.documentBuilderFactory = null;
|
||||
this.saxParserFactory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +121,8 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
|
||||
if (processExternalEntities) {
|
||||
this.supportDtd = true;
|
||||
}
|
||||
this.documentBuilderFactory = null;
|
||||
this.saxParserFactory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,14 +142,13 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
|
||||
*/
|
||||
protected Document buildDocument() {
|
||||
try {
|
||||
DocumentBuilder documentBuilder;
|
||||
synchronized (this.documentBuilderFactoryMonitor) {
|
||||
if (this.documentBuilderFactory == null) {
|
||||
this.documentBuilderFactory = createDocumentBuilderFactory();
|
||||
}
|
||||
documentBuilder = createDocumentBuilder(this.documentBuilderFactory);
|
||||
DocumentBuilderFactory builderFactory = this.documentBuilderFactory;
|
||||
if (builderFactory == null) {
|
||||
builderFactory = createDocumentBuilderFactory();
|
||||
this.documentBuilderFactory = builderFactory;
|
||||
}
|
||||
return documentBuilder.newDocument();
|
||||
DocumentBuilder builder = createDocumentBuilder(builderFactory);
|
||||
return builder.newDocument();
|
||||
}
|
||||
catch (ParserConfigurationException ex) {
|
||||
throw new UnmarshallingFailureException("Could not create document placeholder: " + ex.getMessage(), ex);
|
||||
@@ -179,11 +183,11 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
|
||||
protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory)
|
||||
throws ParserConfigurationException {
|
||||
|
||||
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
if (!isProcessExternalEntities()) {
|
||||
documentBuilder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
|
||||
builder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
|
||||
}
|
||||
return documentBuilder;
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,11 +197,17 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
|
||||
* @throws ParserConfigurationException if thrown by JAXP methods
|
||||
*/
|
||||
protected XMLReader createXmlReader() throws SAXException, ParserConfigurationException {
|
||||
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
|
||||
SAXParser saxParser = saxParserFactory.newSAXParser();
|
||||
SAXParserFactory parserFactory = this.saxParserFactory;
|
||||
if (parserFactory == null) {
|
||||
parserFactory = SAXParserFactory.newInstance();
|
||||
parserFactory.setNamespaceAware(true);
|
||||
parserFactory.setFeature(
|
||||
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
parserFactory.setFeature(
|
||||
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
|
||||
this.saxParserFactory = parserFactory;
|
||||
}
|
||||
SAXParser saxParser = parserFactory.newSAXParser();
|
||||
XMLReader xmlReader = saxParser.getXMLReader();
|
||||
if (!isProcessExternalEntities()) {
|
||||
xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
|
||||
|
||||
@@ -100,9 +100,6 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
|
||||
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
|
||||
|
||||
private static final BufferedReader EMPTY_BUFFERED_READER =
|
||||
new BufferedReader(new StringReader(""));
|
||||
|
||||
/**
|
||||
* Date formats as specified in the HTTP RFC.
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
|
||||
@@ -738,7 +735,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
this.reader = new BufferedReader(sourceReader);
|
||||
}
|
||||
else {
|
||||
this.reader = EMPTY_BUFFERED_READER;
|
||||
this.reader = new BufferedReader(new StringReader(""));
|
||||
}
|
||||
return this.reader;
|
||||
}
|
||||
|
||||
+11
@@ -93,6 +93,17 @@ class MockHttpServletRequestTests {
|
||||
secondRequest.getInputStream().close();
|
||||
}
|
||||
|
||||
@Test // gh-32820
|
||||
void readEmptyReaderWorksAcrossRequests() throws IOException {
|
||||
MockHttpServletRequest firstRequest = new MockHttpServletRequest();
|
||||
firstRequest.getReader().read(new char[256]);
|
||||
firstRequest.getReader().close();
|
||||
|
||||
MockHttpServletRequest secondRequest = new MockHttpServletRequest();
|
||||
secondRequest.getReader().read(new char[256]);
|
||||
secondRequest.getReader().close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void setContentAndGetReader() throws IOException {
|
||||
byte[] bytes = "body".getBytes(Charset.defaultCharset());
|
||||
|
||||
+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;
|
||||
|
||||
+17
-6
@@ -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.
|
||||
@@ -61,6 +61,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @author Arjen Poutsma
|
||||
* @author Sebastien Deleuze
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.0
|
||||
* @see MarshallingHttpMessageConverter
|
||||
*/
|
||||
@@ -70,6 +71,9 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
|
||||
|
||||
private boolean processExternalEntities = false;
|
||||
|
||||
@Nullable
|
||||
private volatile SAXParserFactory sourceParserFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Indicate whether DTD parsing should be supported.
|
||||
@@ -77,6 +81,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
|
||||
*/
|
||||
public void setSupportDtd(boolean supportDtd) {
|
||||
this.supportDtd = supportDtd;
|
||||
this.sourceParserFactory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,6 +102,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
|
||||
if (processExternalEntities) {
|
||||
this.supportDtd = true;
|
||||
}
|
||||
this.sourceParserFactory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,11 +162,16 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
|
||||
if (source instanceof StreamSource streamSource) {
|
||||
InputSource inputSource = new InputSource(streamSource.getInputStream());
|
||||
try {
|
||||
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
String featureName = "http://xml.org/sax/features/external-general-entities";
|
||||
saxParserFactory.setFeature(featureName, isProcessExternalEntities());
|
||||
SAXParserFactory saxParserFactory = this.sourceParserFactory;
|
||||
if (saxParserFactory == null) {
|
||||
saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setFeature(
|
||||
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
saxParserFactory.setFeature(
|
||||
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
|
||||
this.sourceParserFactory = saxParserFactory;
|
||||
}
|
||||
SAXParser saxParser = saxParserFactory.newSAXParser();
|
||||
XMLReader xmlReader = saxParser.getXMLReader();
|
||||
if (!isProcessExternalEntities()) {
|
||||
|
||||
+51
-25
@@ -63,6 +63,7 @@ import org.springframework.util.StreamUtils;
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.0
|
||||
* @param <T> the converted object type
|
||||
*/
|
||||
@@ -75,11 +76,7 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
|
||||
(publicID, systemID, base, ns) -> InputStream.nullInputStream();
|
||||
|
||||
private static final Set<Class<?>> SUPPORTED_CLASSES = Set.of(
|
||||
DOMSource.class,
|
||||
SAXSource.class,
|
||||
StAXSource.class,
|
||||
StreamSource.class,
|
||||
Source.class);
|
||||
DOMSource.class, SAXSource.class, StAXSource.class, StreamSource.class, Source.class);
|
||||
|
||||
|
||||
private final TransformerFactory transformerFactory = TransformerFactory.newInstance();
|
||||
@@ -88,6 +85,15 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
|
||||
|
||||
private boolean processExternalEntities = false;
|
||||
|
||||
@Nullable
|
||||
private volatile DocumentBuilderFactory documentBuilderFactory;
|
||||
|
||||
@Nullable
|
||||
private volatile SAXParserFactory saxParserFactory;
|
||||
|
||||
@Nullable
|
||||
private volatile XMLInputFactory xmlInputFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes}
|
||||
@@ -104,6 +110,9 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
|
||||
*/
|
||||
public void setSupportDtd(boolean supportDtd) {
|
||||
this.supportDtd = supportDtd;
|
||||
this.documentBuilderFactory = null;
|
||||
this.saxParserFactory = null;
|
||||
this.xmlInputFactory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,6 +133,9 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
|
||||
if (processExternalEntities) {
|
||||
this.supportDtd = true;
|
||||
}
|
||||
this.documentBuilderFactory = null;
|
||||
this.saxParserFactory = null;
|
||||
this.xmlInputFactory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,17 +177,21 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
|
||||
|
||||
private DOMSource readDOMSource(InputStream body, HttpInputMessage inputMessage) throws IOException {
|
||||
try {
|
||||
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
documentBuilderFactory.setNamespaceAware(true);
|
||||
documentBuilderFactory.setFeature(
|
||||
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
documentBuilderFactory.setFeature(
|
||||
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
|
||||
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
|
||||
if (!isProcessExternalEntities()) {
|
||||
documentBuilder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
|
||||
DocumentBuilderFactory builderFactory = this.documentBuilderFactory;
|
||||
if (builderFactory == null) {
|
||||
builderFactory = DocumentBuilderFactory.newInstance();
|
||||
builderFactory.setNamespaceAware(true);
|
||||
builderFactory.setFeature(
|
||||
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
builderFactory.setFeature(
|
||||
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
|
||||
this.documentBuilderFactory = builderFactory;
|
||||
}
|
||||
Document document = documentBuilder.parse(body);
|
||||
DocumentBuilder builder = builderFactory.newDocumentBuilder();
|
||||
if (!isProcessExternalEntities()) {
|
||||
builder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
|
||||
}
|
||||
Document document = builder.parse(body);
|
||||
return new DOMSource(document);
|
||||
}
|
||||
catch (NullPointerException ex) {
|
||||
@@ -197,11 +213,17 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
|
||||
|
||||
private SAXSource readSAXSource(InputStream body, HttpInputMessage inputMessage) throws IOException {
|
||||
try {
|
||||
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
|
||||
SAXParser saxParser = saxParserFactory.newSAXParser();
|
||||
SAXParserFactory parserFactory = this.saxParserFactory;
|
||||
if (parserFactory == null) {
|
||||
parserFactory = SAXParserFactory.newInstance();
|
||||
parserFactory.setNamespaceAware(true);
|
||||
parserFactory.setFeature(
|
||||
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
|
||||
parserFactory.setFeature(
|
||||
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
|
||||
this.saxParserFactory = parserFactory;
|
||||
}
|
||||
SAXParser saxParser = parserFactory.newSAXParser();
|
||||
XMLReader xmlReader = saxParser.getXMLReader();
|
||||
if (!isProcessExternalEntities()) {
|
||||
xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
|
||||
@@ -217,11 +239,15 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
|
||||
|
||||
private Source readStAXSource(InputStream body, HttpInputMessage inputMessage) {
|
||||
try {
|
||||
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
|
||||
inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, isSupportDtd());
|
||||
inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, isProcessExternalEntities());
|
||||
if (!isProcessExternalEntities()) {
|
||||
inputFactory.setXMLResolver(NO_OP_XML_RESOLVER);
|
||||
XMLInputFactory inputFactory = this.xmlInputFactory;
|
||||
if (inputFactory == null) {
|
||||
inputFactory = XMLInputFactory.newInstance();
|
||||
inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, isSupportDtd());
|
||||
inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, isProcessExternalEntities());
|
||||
if (!isProcessExternalEntities()) {
|
||||
inputFactory.setXMLResolver(NO_OP_XML_RESOLVER);
|
||||
}
|
||||
this.xmlInputFactory = inputFactory;
|
||||
}
|
||||
XMLStreamReader streamReader = inputFactory.createXMLStreamReader(body);
|
||||
return new StAXSource(streamReader);
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
|
||||
+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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user