Compare commits

...

80 Commits

Author SHA1 Message Date
Spring Builds 06b492dc0e Release v6.1.9 2024-06-13 10:15:12 +00:00
Arjen Poutsma 6f32ff489a Use null stream in ReactorNettyClientResponse if no body is available
Closes gh-32805
2024-06-13 11:10:50 +02:00
Juergen Hoeller 24c8dfea1f Remove duplicated javadoc paragraph 2024-06-12 14:23:26 +02:00
Juergen Hoeller cdfe5816c8 Upgrade to Netty 4.1.111 and Awaitility 4.2.1 2024-06-12 14:23:14 +02:00
Juergen Hoeller 0ff200b2f1 Trigger cancellation on context close for non-managed objects only
Specifically for prototype/scoped beans and FactoryBean-exposed objects.

Closes gh-33009
2024-06-12 13:31:00 +02:00
Stéphane Nicoll 261dac87cc Upgrade to Reactor 2023.0.7
Closes gh-33007
2024-06-11 14:42:08 +02:00
Juergen Hoeller fce2f49e46 Polishing (aligned with main) 2024-06-11 09:07:09 +02:00
Juergen Hoeller eca2b9657e Documentation notes for @Bean overriding and bean name matching
Closes gh-32825
2024-06-11 09:06:33 +02:00
Juergen Hoeller e48f37d956 Upgrade to Micrometer 1.12.7
Closes gh-33001
2024-06-11 09:05:48 +02:00
Juergen Hoeller ddc397dd05 Upgrade to spring-javaformat-checkstyle 0.0.42 2024-06-11 09:04:30 +02:00
Stéphane Nicoll 6b7f0bd4b6 Fix typo 2024-06-11 06:47:42 +02:00
Sébastien Deleuze c97a895f09 Add support for double backslashes to StringUtils#cleanPath
Closes gh-32962
2024-06-10 22:25:31 +02:00
rstoyanchev 3b13f2ed38 Update code-of-conduct email 2024-06-10 10:48:06 +01:00
Stéphane Nicoll a0eebca0cf Merge pull request #32993 from hlmg
* pr/32993:
  Fix typo

Closes gh-32993
2024-06-10 10:02:36 +02:00
hlmg c6c64e6fe7 Fix typo
See gh-32993
2024-06-10 10:00:37 +02:00
Brian Clozel 0ca393c0dc Restrict memory allocation in ContentCachingRequestWrapper
Prior to this commit, the `ContentCachingRequestWrapper` could allocate
a `FastByteArrayOutputStream` block that was larger than the content
cache limit given as a consturctor argument. This was due to an
optimization applied in gh-31834 for allocating the right content cache
size when the request size is known.

Fixes gh-32987
2024-06-10 09:46:01 +02:00
Brian Clozel 6681394886 Stop observations for async requests in Servlet filter
Prior to this commit, the `ServerHttpObservationFilter` would support
async dispatches and would do the following:

1. start the observation
2. call the filter chain
3. if async has started, do nothing
4. if not in async mode, stop the observation

This behavior would effectively rely on Async implementations to
complete and dispatch the request back to the container for an async
dispatch. This is what Spring web frameworks do and guarantee.

Some implementations complete the async request but do not dispatch
back; as a result, observations could leak as they are never stopped.

This commit changes the support of async requests. The filter now
opts-out of async dispatches - the filter will not be called for those
anymore. Instead, if the application started async mode during the
initial container dispatch, the filter will register an AsyncListener to
be notified of the outcome of the async handling.

Fixes gh-32730
2024-06-07 19:01:57 +02:00
Sébastien Deleuze 172987c874 Ignore checkpointOnRefresh after restore
Closes gh-32978
2024-06-07 18:41:37 +02:00
Thomas Deblock 47a5ebfde6 Support canEncode() for JAXBElement in Jaxb2XmlEncoder
Commit d7970e4ab8 introduced support for JAXBElement in
Jaxb2XmlEncoder's encodeValue() method; however, canEncode() still
returned false for a JAXBElement element type.

This commit revises canEncode() so that it returns true for an element
type that is assignable from JAXBElement.

See gh-30552
See gh-32972
Closes gh-32977
2024-06-07 14:13:59 +02:00
Sébastien Deleuze 43a113f067 Polishing
See gh-32983
2024-06-07 13:45:09 +02:00
Sébastien Deleuze dc250e1cc1 Remove outdated copyright from index.adoc
Closes gh-32983
2024-06-07 13:35:21 +02:00
Sébastien Deleuze fe74fcfded Exclude node_modules from NoHttp checks
Closes gh-32980
2024-06-07 11:43:45 +02:00
Juergen Hoeller 2451bd62b0 Polishing 2024-06-06 20:43:31 +02:00
Juergen Hoeller 624d6dd167 Expose actual result value for @CacheEvict condition
Closes gh-32960
2024-06-06 20:43:04 +02:00
Juergen Hoeller 0ea96b4806 Skip ajc-compiled aspects for ajc-compiled target classes
Includes defensive ignoring of incompatible aspect types.

Closes gh-32970
2024-06-06 20:42:07 +02:00
Sébastien Deleuze c28a0d5627 Add missing hints for Hibernate @TenantId
Closes gh-32967
2024-06-06 10:04:35 +02:00
Juergen Hoeller 61d045ce52 Polishing 2024-06-06 08:54:37 +02:00
Juergen Hoeller c0bef2c693 Lazily start resources on demand (if necessary outside of lifecycle)
See gh-32945
2024-06-06 08:54:32 +02:00
Stéphane Nicoll c3a0eaa95e Merge pull request #32966 from ypyf
* pr/32966:
  Use HttpStatusCode consistently in reference guide

Closes gh-32966
2024-06-06 08:08:45 +02:00
ypyf e12d1259d1 Use HttpStatusCode consistently in reference guide
See gh-32966
2024-06-06 08:06:33 +02:00
Brian Clozel 404c4d9d92 Support @Valid on container elements for handler arguments
Prior to this commit, #31870 added support for constraint annotations on
container elements for handler method argument validation. Supporting
this use case:

```
public void addNames(List<@NotEmpty String> names)
```

This commit does the same for `@Valid` annotation:

```
public void addPeople(List<@Valid Person> people)
```

Fixes gh-32964
2024-06-05 20:02:46 +02:00
Juergen Hoeller 7785f94c4c Revise and align Reactor client lifecycle management
Closes gh-32945
2024-06-05 16:32:40 +02:00
Juergen Hoeller 4f6f2c0d41 Revert to separate get/put steps against method cache for concurrency
Closes gh-32958
2024-06-05 16:32:27 +02:00
Stéphane Nicoll e6da2a86fc Merge pull request #32957 from soglad
* pr/32957:
  Fix entity name in MappingSqlQuery example of reference guide

Closes gh-32957
2024-06-05 10:45:01 +02:00
Tony Wen 301087e510 Fix entity name in MappingSqlQuery example of reference guide
See gh-32957
2024-06-05 10:43:14 +02:00
Juergen Hoeller 6c054f88ea Defensively handle UncheckedIOException cause (for NullAway compliance) 2024-06-05 00:01:52 +02:00
Juergen Hoeller f58c7d80cc Upgrade to SLF4J 2.0.13, Jetty 12.0.10, Netty 4.1.110, JRuby 9.4.7 2024-06-04 23:44:54 +02:00
Juergen Hoeller e5be10d53d Consistently throw IOException from ReactorNettyClientResponse
Aligned with ReactorNettyClientRequest.

See gh-32952
2024-06-04 23:43:10 +02:00
Juergen Hoeller 524da905db Consistently throw IOException from ReactorNettyClientRequest
This commit renames ReactorNettyClientRequestFactoryTests.

Closes gh-32952
2024-06-04 22:59:29 +02:00
Juergen Hoeller 4323c60513 Common context lifecycle management for ReactorResourceFactory
This commit moves ReactorResourceFactoryTests to same package.

Closes gh-32945
2024-06-04 22:59:18 +02:00
Sébastien Deleuze f6b608eecb Consistently support Hibernate annotation hint inference on methods
See gh-32842
2024-06-04 16:46:25 +02:00
Sébastien Deleuze 4da1511ed3 Infer hints for Hibernate generators
This commit updates
PersistenceManagedTypesBeanRegistrationAotProcessor
in order to infer hints for Hibernate annotations meta
annotated with `@ValueGenerationType` (like `@CreationTimestamp`)
and `@IdGeneratorType`.

`@GenericGenerator` is not supported as it is deprecated as of
Hibernate 6.5.

Closes gh-32842
2024-06-04 16:46:25 +02:00
Stéphane Nicoll 7102c33661 Add section about using complex data structures with AOT
Closes gh-32273
2024-06-04 15:51:40 +02:00
Sébastien Deleuze 43409b00d0 Refine KotlinDetector.isKotlinType documentation
This commit documents changes in lambda detection
as of Kotlin 2.0.

Closes gh-32905
2024-06-03 18:58:13 +02:00
Sébastien Deleuze d55abc6cf9 Fix RegisterReflectionForBinding Javadoc
Closes gh-32947
2024-06-03 18:26:24 +02:00
Sébastien Deleuze 31806f3a6b Fix MethodValidationPostProcessor refdoc
Closes gh-32929
2024-06-03 18:20:52 +02:00
Juergen Hoeller 8a84241c1e Polishing 2024-06-03 12:46:31 +02:00
Juergen Hoeller 624be6d4e6 Report bean creation failure in sortAdvisors as AopConfigException
Closes gh-32230
2024-06-03 12:46:14 +02:00
Juergen Hoeller b08883b65c Avoid NoSuchMethodException for annotation attribute checks
Closes gh-32921
2024-06-03 12:45:11 +02:00
Cong-Xin Cynthia Qiu 542ba3517f Fix typo in Jakarta validation documentation
Closes gh-32928
2024-05-31 12:22:02 +02:00
Juergen Hoeller 8c6a7799be Upgrade to Checkstyle 10.17 2024-05-28 18:46:22 +02:00
Juergen Hoeller 557dbba585 Remove superfluous addToClassHierarchy call for Enum types
Closes gh-32906
2024-05-28 18:45:05 +02:00
Sam Brannen e9de426eb5 Support compilation of map indexing with primitive in SpEL
Prior to this commit, the Spring Expression Language (SpEL) failed to
compile an expression that indexed into a Map using a primitive literal
(boolean, int, long, float, or double).

This commit adds support for compilation of such expressions by
ensuring that primitive literals are boxed into their corresponding
wrapper types in the compiled bytecode.

Closes gh-32903
2024-05-28 10:19:15 +02:00
Sam Brannen cda577d1aa Support compilation of array and list indexing with Integer in SpEL
Prior to this commit, the Spring Expression Language (SpEL) failed to
compile an expression that indexed into any array or list using an
Integer.

This commit adds support for compilation of such expressions by
ensuring that an Integer is unboxed into an int in the compiled
bytecode.

See gh-32694
Closes gh-32908
2024-05-27 17:06:48 +02:00
Sam Brannen 8feb842df5 Upgrade to AssertJ 3.26.0
See https://github.com/assertj/assertj/issues/3322
2024-05-27 16:43:22 +02:00
Sam Brannen ea2931f24a Use Develocity Gradle plugin API to avoid deprecation warning
Prior to this commit, the Gradle build issued the following warning.

- The deprecated "gradleEnterprise.buildScan.value" API has been
  replaced by "develocity.buildScan.value"
2024-05-24 15:41:58 +02:00
Juergen Hoeller 26d1c38d84 Polishing 2024-05-24 13:05:49 +02:00
Juergen Hoeller 345daaabbc Detect original generic method for CGLIB bridge method
Closes gh-32888
2024-05-24 11:49:10 +02:00
Juergen Hoeller 6c08d93992 Polishing 2024-05-23 17:07:55 +02:00
Juergen Hoeller 6d7cd9c7dc Defensive handling of incompatible advice methods
This covers AspectJ transaction and caching aspects when encountered by Spring AOP.

Closes gh-32882
See gh-32793
2024-05-23 17:07:51 +02:00
Attacktive 73eb6f0660 Complete a Kotlin code snippet in the refdoc
Closes gh-32877
2024-05-23 16:03:34 +02:00
Stéphane Nicoll 31f298b929 Merge pull request #32874 from Seungpang
* pr/32874:
  Polish
  Document using ThreadLocal#remove instead of ThreadLocal#set(null)

Closes gh-32874
2024-05-23 08:14:00 +02:00
Stéphane Nicoll c01aab5850 Polish
See gh-32874
2024-05-23 08:12:30 +02:00
Seungrae 61ef5a8930 Document using ThreadLocal#remove instead of ThreadLocal#set(null)
See gh-32874
2024-05-23 08:10:54 +02:00
Spring Builds 3b53ee7038 Next development version (v6.1.9-SNAPSHOT) 2024-05-22 16:45:49 +00:00
rstoyanchev 89dd247b97 Improve docs on controller method validation
Closes gh-32807
2024-05-22 17:15:54 +01:00
Stéphane Nicoll 39dd1e4049 Remove outdated Javadoc links
Closes gh-32872
2024-05-22 14:40:25 +02:00
Rob Winch 34f4ad3b71 Modernize Antora Build
- Use same playbook as docs-build
- Use Env Variables to cause partial build (same as docs-build)
- Use package.json so that dependencies can be updated with dependabot
2024-05-22 10:20:20 +02:00
Juergen Hoeller ea596aa211 Select most specific advice method in case of override
Closes gh-32865
2024-05-22 10:00:31 +02:00
Juergen Hoeller 58da30cd30 Upgrade to Jetty Reactive HttpClient 4.0.4 2024-05-21 19:22:20 +02:00
Juergen Hoeller cd33b4e35a Polishing 2024-05-21 18:25:57 +02:00
Juergen Hoeller 20dea0dae2 Polishing 2024-05-21 17:39:11 +02:00
Juergen Hoeller fee17e11ba Default fallback parsing for UTC without milliseconds
Closes gh-32856
2024-05-21 17:39:06 +02:00
Juergen Hoeller 65e1337d35 Polishing 2024-05-21 11:16:25 +02:00
Juergen Hoeller a4c2f291d9 Avoid creation of SAXParserFactory for every read operation
Includes JAXBContext locking revision (avoiding synchronization) and consistent treatment of DocumentBuilderFactory (in terms of caching as well as locking).

Closes gh-32851
2024-05-21 11:16:19 +02:00
Stéphane Nicoll f26483d272 Detect deprecated element in generic types
This commit updates Spring AOT to suppress a deprecation warning for
a generic type that has a deprecated element. Previously we only were
checking for the raw class.

Closes gh-32850
2024-05-21 08:53:18 +02:00
Stéphane Nicoll 481d036f7a Avoid reader on empty content to be shared by multiple requests
This commit avoids several instances of MockHttpServletRequest to
have a common reader for empty content as closing it will have an
unwanted side effect on the others.

Closes gh-32820
2024-05-20 13:58:36 +02:00
Sébastien Deleuze 2a2ef443a5 Refine CDS documentation
Closes gh-32843
2024-05-20 10:24:29 +02:00
Juergen Hoeller 617833bec9 Defensively catch and log pointcut parsing exceptions
Closes gh-32838
See gh-32793
2024-05-17 12:27:59 +02:00
Spring Builds 4d633c2ea8 Next development version (v6.1.8-SNAPSHOT) 2024-05-16 09:04:43 +00:00
103 changed files with 1672 additions and 672 deletions
+2
View File
@@ -51,3 +51,5 @@ atlassian-ide-plugin.xml
.vscode/
cached-antora-playbook.yml
node_modules
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
-2
View File
@@ -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 -1
View File
@@ -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();
+39
View File
@@ -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
+3 -20
View File
@@ -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
@@ -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`
+1 -3
View File
@@ -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:
+10
View File
@@ -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"
}
}
+10 -10
View File
@@ -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
View File
@@ -1,4 +1,4 @@
version=6.1.7-SNAPSHOT
version=6.1.9
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -29,7 +29,7 @@ publishing {
developer {
id = "jhoeller"
name = "Juergen Hoeller"
email = "jhoeller@pivotal.io"
email = "juergen.hoeller@broadcom.com"
}
}
issueManagement {
+2 -2
View File
@@ -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
}
}
@@ -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");
}
@@ -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);
}
{
@@ -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
@@ -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) {
@@ -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));
}
}
}
@@ -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;
}
/**
@@ -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;
}
@@ -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) {
@@ -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 {
@@ -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);
@@ -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");
}
@@ -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");
}
@@ -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>
@@ -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>
@@ -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>
@@ -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;
}
}
}
@@ -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) -> {})));
@@ -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");
@@ -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");
});
}
@@ -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;
}
@@ -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);
}
@@ -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;
}
@@ -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);
}
}
@@ -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();
}
}
}
@@ -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
@@ -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);
}
}
@@ -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 {
@@ -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("");
@@ -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;
}
@@ -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;
@@ -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;
}
}
@@ -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 {
@@ -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>
@@ -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 &mdash; for example:
* <p>The annotated element can be any Spring bean class or method &mdash; for example:
*
* <pre class="code">
* &#064;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;
}
/**
@@ -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
@@ -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() + "]";
@@ -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) {
@@ -135,7 +135,7 @@ class AbstractRoutingDataSourceTests {
routingDataSource.setDefaultTargetDataSource(ds);
routingDataSource.setLenientFallback(false);
routingDataSource.afterPropertiesSet();
lookupKey.set(null);
lookupKey.remove();
assertThat(routingDataSource.determineTargetDataSource()).isSameAs(ds);
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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());
@@ -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
@@ -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;
}
@@ -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> {
@@ -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
@@ -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;
@@ -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;
}
}
@@ -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
@@ -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;
@@ -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()) {
@@ -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);
@@ -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;
}
}
@@ -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;
}
@@ -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();
@@ -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();
}
}
@@ -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();
}
@@ -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