Compare commits

...

83 Commits

Author SHA1 Message Date
Spring Builds 5356a1b1ac Release v6.1.10 2024-06-19 15:29:59 +00:00
Brian Clozel f7307c9e07 Avoid recording RestClient observations twice
Prior to this commit, the fix for gh-32575 introduced cases where the
client observation would be stopped twice.

This commit ensures that `RestClient` observations are stopped only once
when the response is closed, or before throwing an unhanlded exception.

Fixes gh-33068
2024-06-19 12:46:21 +02:00
Brian Clozel 098c4b1dd7 Use Sonatype S01 token in release pipeline 2024-06-18 19:25:20 +02:00
Juergen Hoeller 65dbfd09b4 Defensive PersistenceExceptionTranslator bean retrieval on shutdown
Closes gh-33067
2024-06-18 18:31:15 +02:00
Sam Brannen 203fa75196 Support all "connection reset" phrases in DisconnectedClientHelper
Prior to this commit, the isClientDisconnectedException() method in
DisconnectedClientHelper checked whether the message of the ultimate
exception in an exception chain contained one of the phrases "broken
pipe" or "connection reset by peer". However, that failed to match if
the exception message contained "Connection reset", which is the case
for the SocketException thrown by throwConnectionReset() in
sun.nio.ch.SocketChannelImpl.

This commit therefore replaces the "connection reset by peer" phrase
with "connection reset" in order to support all exception messages
containing "connection reset".

Closes gh-33064
2024-06-18 16:43:06 +02:00
Juergen Hoeller 9a56a8877f Polishing 2024-06-17 18:42:37 +02:00
Juergen Hoeller e79a9a5bff Correct and consistent event class names in constructor javadoc
Closes gh-33032
2024-06-17 18:42:30 +02:00
Juergen Hoeller 2c3c3831c1 Consistently ignore bridge method on generated subclass for visibility purposes
Closes gh-33030
2024-06-17 18:42:20 +02:00
Arjen Poutsma c38e9896c7 Remove use of ServletException in ModelFactory
This commit changes the use of HttpSessionRequiredException in
ModelFactory::initModel to an IllegalStateException, because the former
extends ServletException and cannot be used in WebFlux.

Closes gh-33043
2024-06-17 14:19:19 +02:00
Stéphane Nicoll 3e0849a566 Fix typo
Closes gh-33050
2024-06-17 13:39:18 +02:00
github-actions[bot] f3a605b92c Update Antora Spring UI to v0.4.16 2024-06-17 10:06:47 +00:00
Stéphane Nicoll 6fdff201e9 Fix property name in Container Extension Points section
Closes gh-33037
2024-06-16 16:47:36 +02:00
Stéphane Nicoll c42778a05b Merge pull request #33036 from tafjwr
* pr/33036:
  Polish "Fix typo in comment"
  Fix typo in comment

Closes gh-33036
2024-06-16 09:59:15 +02:00
Stéphane Nicoll f140df881d Polish "Fix typo in comment"
See gh-33036
2024-06-16 09:57:24 +02:00
tafjwr 12cf654c98 Fix typo in comment
See gh-33036
2024-06-16 09:56:58 +02:00
Juergen Hoeller 089e4e69f1 Do not attempt to load pre-enhanced class for reloadable classes
Closes gh-33024
2024-06-14 22:07:46 +02:00
Stéphane Nicoll 77755ff2fa Remove repo-specific code of conduct
This will allow us to inherit the org's CoC instead.

Closes gh-33031
2024-06-14 18:58:49 +02:00
Stéphane Nicoll d0aa7ad524 Fix invalid character in Javadoc of BeanFactory 2024-06-13 14:06:19 +02:00
Spring Builds 072fc80cdb Next development version (v6.1.10-SNAPSHOT) 2024-06-13 10:27:57 +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
95 changed files with 1393 additions and 545 deletions
-44
View File
@@ -1,44 +0,0 @@
= Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open
and welcoming community, we pledge to respect all people who contribute through reporting
issues, posting feature requests, updating documentation, submitting pull requests or
patches, and other activities.
We are committed to making participation in this project a harassment-free experience for
everyone, regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance, body size, race, ethnicity, age,
religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses,
without explicit permission
* Other unethical or unprofessional conduct
Project maintainers have the right and responsibility to remove, edit, or reject comments,
commits, code, wiki edits, issues, and other contributions that are not aligned to this
Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors
that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and
consistently applying these principles to every aspect of managing this project. Project
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
from the project team.
This Code of Conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will
be reviewed and investigated and will result in a response that is deemed necessary and
appropriate to the circumstances. Maintainers are obligated to maintain confidentiality
with regard to the reporter of an incident.
This Code of Conduct is adapted from the
https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at
https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]
+1 -1
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
+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();
+2 -2
View File
@@ -7,8 +7,8 @@ anchors:
gradle-enterprise-task-params: &gradle-enterprise-task-params
DEVELOCITY_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
sonatype-task-params: &sonatype-task-params
SONATYPE_USERNAME: ((sonatype-username))
SONATYPE_PASSWORD: ((sonatype-password))
SONATYPE_USERNAME: ((s01-user-token))
SONATYPE_PASSWORD: ((s01-user-token-password))
SONATYPE_URL: ((sonatype-url))
SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile))
artifactory-task-params: &artifactory-task-params
+1 -1
View File
@@ -36,4 +36,4 @@ runtime:
failure_level: warn
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.16/ui-bundle.zip
@@ -221,8 +221,8 @@ NOTE: `ThreadLocal` instances come with serious issues (potentially resulting in
incorrectly using them in multi-threaded and multi-classloader environments. You
should always consider wrapping a `ThreadLocal` in some other class and never directly use
the `ThreadLocal` itself (except in the wrapper class). Also, you should
always remember to correctly set and unset (where the latter simply involves a call to
`ThreadLocal.set(null)`) the resource local to the thread. Unsetting should be done in
always remember to correctly set and unset (where the latter involves a call to
`ThreadLocal.remove()`) the resource local to the thread. Unsetting should be done in
any case, since not unsetting it might result in problematic behavior. Spring's
`ThreadLocal` support does this for you and should always be considered in favor of using
`ThreadLocal` instances without other proper handling code.
@@ -326,6 +326,19 @@ However, this is not a best practice and flagging the preferred constructor with
In case you are working on a code base that you cannot modify, you can set the {spring-framework-api}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used.
[[aot.bestpractices.comlext-data-structure]]
=== Avoid Complex Data Structure for Constructor Parameters and Properties
When crafting a `RootBeanDefinition` programmatically, you are not constrained in terms of types that you can use.
For instance, you may have a custom `record` with several properties that your bean takes as a constructor argument.
While this works fine with the regular runtime, AOT does not know how to generate the code of your custom data structure.
A good rule of thumb is to keep in mind that bean definitions are an abstraction on top of several models.
Rather than using such structure, decomposing to simple types or referring to a bean that is built as such is recommended.
As a last resort, you can implement your own `org.springframework.aot.generate.ValueCodeGenerator$Delegate`.
To use it, register its fully qualified name in `META-INF/spring/aot.factories` using the `Delegate` as the key.
[[aot.bestpractices.custom-arguments]]
=== Avoid Creating Bean with Custom Arguments
@@ -153,17 +153,15 @@ Letting qualifier values select against target bean names, within the type-match
candidates, does not require a `@Qualifier` annotation at the injection point.
If there is no other resolution indicator (such as a qualifier or a primary marker),
for a non-unique dependency situation, Spring matches the injection point name
(that is, the field name or parameter name) against the target bean names and chooses the
same-named candidate, if any.
(that is, the field name or parameter name) against the target bean names and chooses
the same-named candidate, if any (either by bean name or by associated alias).
Since version 6.1, this requires the `-parameters` Java compiler flag to be present.
====
That said, if you intend to express annotation-driven injection by name, do not
primarily use `@Autowired`, even if it is capable of selecting by bean name among
type-matching candidates. Instead, use the JSR-250 `@Resource` annotation, which is
semantically defined to identify a specific target component by its unique name, with
the declared type being irrelevant for the matching process. `@Autowired` has rather
As an alternative for injection by name, consider the JSR-250 `@Resource` annotation
which is semantically defined to identify a specific target component by its unique name,
with the declared type being irrelevant for the matching process. `@Autowired` has rather
different semantics: After selecting candidate beans by type, the specified `String`
qualifier value is considered within those type-selected candidates only (for example,
matching an `account` qualifier against beans marked with the same qualifier label).
@@ -75,6 +75,31 @@ lead to concurrent access exceptions, inconsistent state in the bean container,
[[beans-definition-overriding]]
== Overriding Beans
Bean overriding is happening when a bean is registered using an identifier that is
already allocated. While bean overriding is possible, it makes the configuration harder
to read and this feature will be deprecated in a future release.
To disable bean overriding altogether, you can set the `allowBeanDefinitionOverriding`
flag to `false` on the `ApplicationContext` before it is refreshed. In such setup, an
exception is thrown if bean overriding is used.
By default, the container logs every bean overriding at `INFO` level so that you can
adapt your configuration accordingly. While not recommended, you can silence those logs
by setting the `allowBeanDefinitionOverriding` flag to `true`.
.Java-configuration
****
If you use Java Configuration, a corresponding `@Bean` method always silently overrides
a scanned bean class with the same component name as long as the return type of the
`@Bean` method matches that bean class. This simply means that the container will call
the `@Bean` factory method in favor of any pre-declared constructor on the bean class.
****
[[beans-beanname]]
== Naming Beans
@@ -439,7 +439,7 @@ dataSource.url=jdbc:mysql:mydb
----
This example file can be used with a container definition that contains a bean called
`dataSource` that has `driver` and `url` properties.
`dataSource` that has `driverClassName` and `url` properties.
Compound property names are also supported, as long as every component of the path
except the final property being overridden is already non-null (presumably initialized
@@ -299,7 +299,7 @@ Java::
public class AppConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
public static MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
@@ -341,7 +341,7 @@ xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] sections.
=== Method Validation Exceptions
By default, `jakarta.validation.ConstraintViolationException` is raised with the set of
``ConstraintViolation``s returned by `jakarata.validation.Validator`. As an alternative,
``ConstraintViolation``s returned by `jakarta.validation.Validator`. As an alternative,
you can have `MethodValidationException` raised instead with ``ConstraintViolation``s
adapted to `MessageSourceResolvable` errors. To enable set the following flag:
@@ -357,7 +357,7 @@ Java::
public class AppConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
public static MethodValidationPostProcessor validationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setAdaptConstraintViolations(true);
return processor;
@@ -112,7 +112,7 @@ Java::
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
public Actor getActor(Long id) {
return actorMappingQuery.findObject(id);
}
----
@@ -123,11 +123,11 @@ Kotlin::
----
private val actorMappingQuery = ActorMappingQuery(dataSource)
fun getCustomer(id: Long) = actorMappingQuery.findObject(id)
fun getActor(id: Long) = actorMappingQuery.findObject(id)
----
======
The method in the preceding example retrieves the customer with the `id` that is passed in as the
The method in the preceding example retrieves the actor with the `id` that is passed in as the
only parameter. Since we want only one object to be returned, we call the `findObject` convenience
method with the `id` as the parameter. If we had instead a query that returned a
list of objects and took additional parameters, we would use one of the `execute`
+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.
@@ -58,7 +58,7 @@ your `@Configuration` classes, as the following example shows:
By default, the infrastructure looks for a bean named `jmsListenerContainerFactory`
as the source for the factory to use to create message listener containers. In this
case (and ignoring the JMS infrastructure setup), you can invoke the `processOrder`
method with a core poll size of three threads and a maximum pool size of ten threads.
method with a core pool size of three threads and a maximum pool size of ten threads.
You can customize the listener container factory to use for each annotation or you can
configure an explicit default by implementing the `JmsListenerConfigurer` interface.
@@ -97,8 +97,8 @@ Java::
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.onStatus(HttpStatusCode::is4xxClientError, response -> ...)
.onStatus(HttpStatusCode::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
----
@@ -109,8 +109,8 @@ Kotlin::
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.onStatus(HttpStatusCode::is4xxClientError) { ... }
.onStatus(HttpStatusCode::is5xxServerError) { ... }
.awaitBody<Person>()
----
======
@@ -338,7 +338,7 @@ Kotlin::
class WebConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// ...
configurer.defaultCodecs().maxInMemorySize(512 * 1024)
}
}
----
@@ -13,7 +13,7 @@ If configured with a task scheduler, the simple broker supports
https://stomp.github.io/stomp-specification-1.2.html#Heart-beating[STOMP heartbeats].
To configure a scheduler, you can declare your own `TaskScheduler` bean and set it through
the `MessageBrokerRegistry`. Alternatively, you can use the one that is automatically
declared in the built-in WebSocket configuration, however, you'll' need `@Lazy` to avoid
declared in the built-in WebSocket configuration, however, you'll need `@Lazy` to avoid
a cycle between the built-in WebSocket configuration and your
`WebSocketMessageBrokerConfigurer`. For example:
+9 -9
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,7 +110,7 @@ dependencies {
api("org.aspectj:aspectjrt:1.9.22.1")
api("org.aspectj:aspectjtools:1.9.22.1")
api("org.aspectj:aspectjweaver:1.9.22.1")
api("org.awaitility:awaitility:4.2.0")
api("org.awaitility:awaitility:4.2.1")
api("org.bouncycastle:bcpkix-jdk18on:1.72")
api("org.codehaus.jettison:jettison:1.5.4")
api("org.crac:crac:1.4.0")
@@ -130,7 +130,7 @@ dependencies {
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.2")
api("org.javamoney:moneta:1.4.2")
api("org.jruby:jruby:9.4.6.0")
api("org.jruby:jruby:9.4.7.0")
api("org.junit.support:testng-engine:1.0.5")
api("org.mozilla:rhino:1.7.14")
api("org.ogce:xpp3:1.1.6")
@@ -139,7 +139,7 @@ dependencies {
api("org.seleniumhq.selenium:htmlunit-driver:2.70.0")
api("org.seleniumhq.selenium:selenium-java:3.141.59")
api("org.skyscreamer:jsonassert:1.5.1")
api("org.slf4j:slf4j-api:2.0.12")
api("org.slf4j:slf4j-api:2.0.13")
api("org.testng:testng:7.9.0")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-core:0.55")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.1.8-SNAPSHOT
version=6.1.10
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;
@@ -86,6 +87,8 @@ import org.springframework.util.StringUtils;
public class AspectJExpressionPointcut extends AbstractExpressionPointcut
implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware {
private static final String AJC_MAGIC = "ajc$";
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = Set.of(
PointcutPrimitive.EXECUTION,
PointcutPrimitive.ARGS,
@@ -103,6 +106,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
@Nullable
private Class<?> pointcutDeclarationScope;
private boolean aspectCompiledByAjc;
private String[] pointcutParameterNames = new String[0];
private Class<?>[] pointcutParameterTypes = new Class<?>[0];
@@ -134,7 +139,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
* @param paramTypes the parameter types for the pointcut
*/
public AspectJExpressionPointcut(Class<?> declarationScope, String[] paramNames, Class<?>[] paramTypes) {
this.pointcutDeclarationScope = declarationScope;
setPointcutDeclarationScope(declarationScope);
if (paramNames.length != paramTypes.length) {
throw new IllegalStateException(
"Number of pointcut parameter names must match number of pointcut parameter types");
@@ -149,6 +154,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
*/
public void setPointcutDeclarationScope(Class<?> pointcutDeclarationScope) {
this.pointcutDeclarationScope = pointcutDeclarationScope;
this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope);
}
/**
@@ -274,6 +280,11 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
@Override
public boolean matches(Class<?> targetClass) {
if (this.pointcutParsingFailed) {
// Pointcut parsing failed before below -> avoid trying again.
return false;
}
if (this.aspectCompiledByAjc && compiledByAjc(targetClass)) {
// ajc-compiled aspect class for ajc-compiled target class -> already weaved.
return false;
}
@@ -536,6 +547,15 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
return resolveExpression().contains("@annotation");
}
private static boolean compiledByAjc(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (field.getName().startsWith(AJC_MAGIC)) {
return true;
}
}
return false;
}
@Override
public boolean equals(@Nullable Object other) {
@@ -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));
}
}
}
@@ -213,8 +213,16 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
return null;
}
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
try {
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}
catch (IllegalArgumentException | IllegalStateException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring incompatible advice method: " + candidateAdviceMethod, ex);
}
return null;
}
}
@Nullable
@@ -488,20 +488,27 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
* @return a List of MethodInterceptors (may also include InterceptorAndDynamicMethodMatchers)
*/
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
if (this.methodCache == null) {
List<Object> cachedInterceptors;
if (this.methodCache != null) {
// Method-specific cache for method-specific pointcuts
MethodCacheKey cacheKey = new MethodCacheKey(method);
cachedInterceptors = this.methodCache.get(cacheKey);
if (cachedInterceptors == null) {
cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
this, method, targetClass);
this.methodCache.put(cacheKey, cachedInterceptors);
}
}
else {
// Shared cache since there are no method-specific advisors (see below).
List<Object> cachedInterceptors = this.cachedInterceptors;
cachedInterceptors = this.cachedInterceptors;
if (cachedInterceptors == null) {
cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
this, method, targetClass);
this.cachedInterceptors = cachedInterceptors;
}
return cachedInterceptors;
}
// Method-specific cache for method-specific pointcuts
return this.methodCache.computeIfAbsent(new MethodCacheKey(method), k ->
this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(this, method, targetClass));
return cachedInterceptors;
}
/**
@@ -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;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@ import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.refl
* Tests for {@link AspectJAdvisorBeanRegistrationAotProcessor}.
*
* @author Sebastien Deleuze
* @since 6.1
*/
class AspectJAdvisorBeanRegistrationAotProcessorTests {
@@ -43,8 +44,9 @@ class AspectJAdvisorBeanRegistrationAotProcessorTests {
private final RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
@Test
void shouldProcessesAspectJClass() {
void shouldProcessAspectJClass() {
process(AspectJClass.class);
assertThat(reflection().onType(AspectJClass.class).withMemberCategory(MemberCategory.DECLARED_FIELDS))
.accepts(this.runtimeHints);
@@ -2,21 +2,29 @@
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache-3.1.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd">
<aop:aspectj-autoproxy/>
<context:spring-configured/>
<cache:annotation-driven mode="aspectj"/>
<bean id="cacheManager" class="org.springframework.cache.support.NoOpCacheManager"/>
<bean id="myAspect" class="org.springframework.aop.aspectj.autoproxy.CodeStyleAspect" factory-method="aspectOf">
<property name="foo" value="bar"/>
</bean>
<bean id="otherBean" class="java.lang.Object"/>
<bean id="otherBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring"/>
<bean id="yetAnotherBean" class="java.lang.Object"/>
<bean id="yetAnotherBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring"/>
<bean id="configuredBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring" lazy-init="true"/>
</beans>
@@ -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>
@@ -243,7 +243,7 @@ public interface BeanFactory {
* specific type, specify the actual bean type as an argument here and subsequently
* use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options.
* <p>Also, generics matching is strict here, as per the Java assignment rules.
* For lenient fallback matching with unchecked semantics (similar to the ´unchecked´
* For lenient fallback matching with unchecked semantics (similar to the 'unchecked'
* Java compiler warning), consider calling {@link #getBeanProvider(Class)} with the
* raw type as a second step if no full generic match is
* {@link ObjectProvider#getIfAvailable() available} with this variant.
@@ -42,6 +42,8 @@ import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.testfixture.beans.DerivedTestBean;
import org.springframework.beans.testfixture.beans.ITestBean;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceEditor;
import org.springframework.lang.Nullable;
@@ -322,12 +324,13 @@ class BeanUtilsTests {
Order original = new Order("test", List.of("foo", "bar"));
// Create a Proxy that loses the generic type information for the getLineItems() method.
OrderSummary proxy = proxyOrder(original);
OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] {OrderSummary.class}, new OrderInvocationHandler(original));
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List<java.lang.String>");
.contains("java.util.List<java.lang.String>");
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
// Ensure that our custom Proxy works as expected.
assertThat(proxy.getId()).isEqualTo("test");
@@ -340,6 +343,23 @@ class BeanUtilsTests {
assertThat(target.getLineItems()).containsExactly("foo", "bar");
}
@Test // gh-32888
public void copyPropertiesWithGenericCglibClass() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
User user = (User) enhancer.create();
user.setId(1);
user.setName("proxy");
user.setAddress("addr");
User target = new User();
BeanUtils.copyProperties(user, target);
assertThat(target.getId()).isEqualTo(user.getId());
assertThat(target.getName()).isEqualTo(user.getName());
assertThat(target.getAddress()).isEqualTo(user.getAddress());
}
@Test
void copyPropertiesWithEditable() throws Exception {
TestBean tb = new TestBean();
@@ -520,6 +540,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerHolder {
@@ -534,6 +555,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class WildcardListHolder1 {
@@ -548,6 +570,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class WildcardListHolder2 {
@@ -562,6 +585,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class NumberUpperBoundedWildcardListHolder {
@@ -576,6 +600,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class NumberListHolder {
@@ -590,6 +615,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerListHolder1 {
@@ -604,6 +630,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerListHolder2 {
@@ -618,6 +645,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class LongListHolder {
@@ -798,6 +826,7 @@ class BeanUtilsTests {
}
}
private static class BeanWithNullableTypes {
private Integer counter;
@@ -828,6 +857,7 @@ class BeanUtilsTests {
}
}
private static class BeanWithPrimitiveTypes {
private boolean flag;
@@ -840,7 +870,6 @@ class BeanUtilsTests {
private char character;
private String text;
@SuppressWarnings("unused")
public BeanWithPrimitiveTypes(boolean flag, byte byteCount, short shortCount, int intCount, long longCount,
float floatCount, double doubleCount, char character, String text) {
@@ -891,21 +920,22 @@ class BeanUtilsTests {
public String getText() {
return text;
}
}
private static class PrivateBeanWithPrivateConstructor {
private PrivateBeanWithPrivateConstructor() {
}
}
@SuppressWarnings("unused")
private static class Order {
private String id;
private List<String> lineItems;
private List<String> lineItems;
Order() {
}
@@ -937,6 +967,7 @@ class BeanUtilsTests {
}
}
private interface OrderSummary {
String getId();
@@ -945,17 +976,10 @@ class BeanUtilsTests {
}
private OrderSummary proxyOrder(Order order) {
return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] { OrderSummary.class }, new OrderInvocationHandler(order));
}
private static class OrderInvocationHandler implements InvocationHandler {
private final Order order;
OrderInvocationHandler(Order order) {
this.order = order;
}
@@ -973,4 +997,46 @@ class BeanUtilsTests {
}
}
private static class GenericBaseModel<T> {
private T id;
private String name;
public T getId() {
return id;
}
public void setId(T id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
private static class User extends GenericBaseModel<Integer> {
private String address;
public User() {
super();
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
}
@@ -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;
}
@@ -49,6 +49,7 @@ import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.cglib.proxy.NoOp;
import org.springframework.cglib.transform.ClassEmitterTransformer;
import org.springframework.cglib.transform.TransformingClassGenerator;
import org.springframework.core.SmartClassLoader;
import org.springframework.lang.Nullable;
import org.springframework.objenesis.ObjenesisException;
import org.springframework.objenesis.SpringObjenesis;
@@ -132,13 +133,21 @@ class ConfigurationClassEnhancer {
enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
enhancer.setUseFactory(false);
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(true);
enhancer.setAttemptLoad(!isClassReloadable(configSuperClass, classLoader));
enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
enhancer.setCallbackFilter(CALLBACK_FILTER);
enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
return enhancer;
}
/**
* Checks whether the given configuration class is reloadable.
*/
private boolean isClassReloadable(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
return (classLoader instanceof SmartClassLoader smartClassLoader &&
smartClassLoader.isClassReloadable(configSuperClass));
}
/**
* Uses enhancer to generate a subclass of superclass,
* ensuring that callbacks are registered for the new subclass.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationEvent;
public abstract class ApplicationContextEvent extends ApplicationEvent {
/**
* Create a new ContextStartedEvent.
* Create a new {@code ApplicationContextEvent}.
* @param source the {@code ApplicationContext} that the event is raised for
* (must not be {@code null})
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationContext;
public class ContextClosedEvent extends ApplicationContextEvent {
/**
* Creates a new ContextClosedEvent.
* Create a new {@code ContextClosedEvent}.
* @param source the {@code ApplicationContext} that has been closed
* (must not be {@code null})
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import org.springframework.context.ApplicationContext;
public class ContextRefreshedEvent extends ApplicationContextEvent {
/**
* Create a new ContextRefreshedEvent.
* Create a new {@code ContextRefreshedEvent}.
* @param source the {@code ApplicationContext} that has been initialized
* or refreshed (must not be {@code null})
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext;
public class ContextStartedEvent extends ApplicationContextEvent {
/**
* Create a new ContextStartedEvent.
* Create a new {@code ContextStartedEvent}.
* @param source the {@code ApplicationContext} that has been started
* (must not be {@code null})
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext;
public class ContextStoppedEvent extends ApplicationContextEvent {
/**
* Create a new ContextStoppedEvent.
* Create a new {@code ContextStoppedEvent}.
* @param source the {@code ApplicationContext} that has been stopped
* (must not be {@code null})
*/
@@ -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);
}
@@ -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();
}
}
}
@@ -66,7 +66,7 @@ public class DefaultBindingErrorProcessor implements BindingErrorProcessor {
@Override
public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) {
// Create field error with the exceptions's code, e.g. "typeMismatch".
// Create field error with the code of the exception, e.g. "typeMismatch".
String field = ex.getPropertyName();
Assert.state(field != null, "No field in exception");
String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field);
@@ -234,8 +234,8 @@ public class MethodValidationAdapter implements MethodValidator {
@Override
public final MethodValidationResult validateArguments(
Object target, Method method, @Nullable MethodParameter[] parameters, Object[] arguments,
Class<?>[] groups) {
Object target, Method method, @Nullable MethodParameter[] parameters,
Object[] arguments, Class<?>[] groups) {
Set<ConstraintViolation<Object>> violations =
invokeValidatorForArguments(target, method, arguments, groups);
@@ -256,23 +256,21 @@ public class MethodValidationAdapter implements MethodValidator {
Object target, Method method, Object[] arguments, Class<?>[] groups) {
ExecutableValidator execVal = this.validator.get().forExecutables();
Set<ConstraintViolation<Object>> violations;
try {
violations = execVal.validateParameters(target, method, arguments, groups);
return execVal.validateParameters(target, method, arguments, groups);
}
catch (IllegalArgumentException ex) {
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
// Let's try to find the bridged method on the implementation class...
Method bridgedMethod = BridgeMethodResolver.getMostSpecificMethod(method, target.getClass());
violations = execVal.validateParameters(target, bridgedMethod, arguments, groups);
return execVal.validateParameters(target, bridgedMethod, arguments, groups);
}
return violations;
}
@Override
public final MethodValidationResult validateReturnValue(
Object target, Method method, @Nullable MethodParameter returnType, @Nullable Object returnValue,
Class<?>[] groups) {
Object target, Method method, @Nullable MethodParameter returnType,
@Nullable Object returnValue, Class<?>[] groups) {
Set<ConstraintViolation<Object>> violations =
invokeValidatorForReturnValue(target, method, returnValue, groups);
@@ -305,9 +303,9 @@ public class MethodValidationAdapter implements MethodValidator {
Map<Path.Node, ParamErrorsBuilder> nestedViolations = new LinkedHashMap<>();
for (ConstraintViolation<Object> violation : violations) {
Iterator<Path.Node> itr = violation.getPropertyPath().iterator();
while (itr.hasNext()) {
Path.Node node = itr.next();
Iterator<Path.Node> nodes = violation.getPropertyPath().iterator();
while (nodes.hasNext()) {
Path.Node node = nodes.next();
MethodParameter parameter;
if (node.getKind().equals(ElementKind.PARAMETER)) {
@@ -328,8 +326,8 @@ public class MethodValidationAdapter implements MethodValidator {
// https://github.com/jakartaee/validation/issues/194
Path.Node parameterNode = node;
if (itr.hasNext()) {
node = itr.next();
if (nodes.hasNext()) {
node = nodes.next();
}
Object value;
@@ -425,7 +423,6 @@ public class MethodValidationAdapter implements MethodValidator {
* @return the name to use
*/
String resolveName(MethodParameter parameter, @Nullable Object value);
}
@@ -456,6 +453,7 @@ public class MethodValidationAdapter implements MethodValidator {
public ParamValidationResultBuilder(
Object target, MethodParameter parameter, @Nullable Object value, @Nullable Object container,
@Nullable Integer containerIndex, @Nullable Object containerKey) {
this.target = target;
this.parameter = parameter;
this.value = value;
@@ -473,7 +471,6 @@ public class MethodValidationAdapter implements MethodValidator {
this.parameter, this.value, this.resolvableErrors, this.container,
this.containerIndex, this.containerKey);
}
}
@@ -527,8 +524,7 @@ public class MethodValidationAdapter implements MethodValidator {
/**
* Default algorithm to select an object name, as described in
* {@link #setObjectNameResolver(ObjectNameResolver)}.
* Default algorithm to select an object name, as described in {@link #setObjectNameResolver}.
*/
private static class DefaultObjectNameResolver implements ObjectNameResolver {
@@ -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 {
@@ -0,0 +1,87 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.annotation;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureClassLoader;
import org.junit.jupiter.api.Test;
import org.springframework.core.SmartClassLoader;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Phillip Webb
* @author Juergen Hoeller
*/
class ConfigurationClassEnhancerTests {
@Test
void enhanceReloadedClass() throws Exception {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader parentClassLoader = getClass().getClassLoader();
CustomClassLoader classLoader = new CustomClassLoader(parentClassLoader);
Class<?> myClass = parentClassLoader.loadClass(MyConfig.class.getName());
configurationClassEnhancer.enhance(myClass, parentClassLoader);
Class<?> myReloadedClass = classLoader.loadClass(MyConfig.class.getName());
Class<?> enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader);
assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader);
}
@Configuration
static class MyConfig {
@Bean
public String myBean() {
return "bean";
}
}
static class CustomClassLoader extends SecureClassLoader implements SmartClassLoader {
CustomClassLoader(ClassLoader parent) {
super(parent);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.contains("MyConfig")) {
String path = name.replace('.', '/').concat(".class");
try (InputStream in = super.getResourceAsStream(path)) {
byte[] bytes = StreamUtils.copyToByteArray(in);
if (bytes.length > 0) {
return defineClass(name, bytes, 0, bytes.length);
}
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
return super.loadClass(name, resolve);
}
@Override
public boolean isClassReloadable(Class<?> clazz) {
return clazz.getName().contains("MyConfig");
}
}
}
@@ -26,6 +26,7 @@ import jakarta.inject.Provider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -43,6 +44,7 @@ import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import static org.assertj.core.api.Assertions.assertThat;
@@ -183,6 +185,14 @@ class AutowiredConfigurationTests {
context.close();
}
@Test
void testValueInjectionWithAccidentalAutowiredAnnotations() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(ValueConfigWithAccidentalAutowiredAnnotations.class);
doTestValueInjection(context);
context.close();
}
private void doTestValueInjection(BeanFactory context) {
System.clearProperty("myProp");
@@ -494,6 +504,32 @@ class AutowiredConfigurationTests {
}
@Configuration
static class ValueConfigWithAccidentalAutowiredAnnotations implements InitializingBean {
boolean invoked;
@Override
public void afterPropertiesSet() {
Assert.state(!invoked, "Factory method must not get invoked on startup");
}
@Bean @Scope("prototype")
@Autowired
public TestBean testBean(@Value("#{systemProperties[myProp]}") Provider<String> name) {
invoked = true;
return new TestBean(name.get());
}
@Bean @Scope("prototype")
@Autowired
public TestBean testBean2(@Value("#{systemProperties[myProp]}") Provider<String> name2) {
invoked = true;
return new TestBean(name2.get());
}
}
@Configuration
static class PropertiesConfig {
@@ -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 {
@@ -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));
@@ -272,8 +276,13 @@ public final class BridgeMethodResolver {
*/
public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) {
if (bridgeMethod == bridgedMethod) {
// Same method: for common purposes, return true to proceed as if it was a visibility bridge.
return true;
}
if (ClassUtils.getUserClass(bridgeMethod.getDeclaringClass()) != bridgeMethod.getDeclaringClass()) {
// Method on generated subclass: return false to consistently ignore it for visibility purposes.
return false;
}
return (bridgeMethod.getReturnType().equals(bridgedMethod.getReturnType()) &&
bridgeMethod.getParameterCount() == bridgedMethod.getParameterCount() &&
Arrays.equals(bridgeMethod.getParameterTypes(), bridgedMethod.getParameterTypes()));
@@ -84,6 +84,10 @@ public abstract class KotlinDetector {
/**
* Determine whether the given {@code Class} is a Kotlin type
* (with Kotlin metadata present on it).
*
* <p>As of Kotlin 2.0, this method can't be used to detect Kotlin
* lambdas unless they are annotated with <code>@JvmSerializableLambda</code>
* as invokedynamic has become the default method for lambda generation.
*/
public static boolean isKotlinType(Class<?> clazz) {
return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null);
@@ -1052,16 +1052,16 @@ public abstract class AnnotationUtils {
return null;
}
try {
Method method = annotation.annotationType().getDeclaredMethod(attributeName);
return invokeAnnotationMethod(method, annotation);
}
catch (NoSuchMethodException ex) {
return null;
for (Method method : annotation.annotationType().getDeclaredMethods()) {
if (method.getName().equals(attributeName) && method.getParameterCount() == 0) {
return invokeAnnotationMethod(method, annotation);
}
}
}
catch (Throwable ex) {
handleValueRetrievalFailure(annotation, ex);
return null;
}
return null;
}
/**
@@ -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);
}
}
}
@@ -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();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationNotAllowedException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
@@ -146,7 +147,14 @@ public class PersistenceExceptionTranslationInterceptor
if (translator == null) {
Assert.state(this.beanFactory != null,
"Cannot use PersistenceExceptionTranslator autodetection without ListableBeanFactory");
translator = detectPersistenceExceptionTranslators(this.beanFactory);
try {
translator = detectPersistenceExceptionTranslators(this.beanFactory);
}
catch (BeanCreationNotAllowedException ex2) {
// Cannot create PersistenceExceptionTranslator bean on shutdown:
// fall back to rethrowing original exception without translation
throw ex;
}
this.persistenceExceptionTranslator = translator;
}
throw DataAccessUtils.translateIfNecessary(ex, translator);
@@ -18,6 +18,7 @@ package org.springframework.dao.annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
@@ -29,6 +30,7 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.ChainedPersistenceExceptionTranslator;
import org.springframework.dao.support.PersistenceExceptionTranslationInterceptor;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.stereotype.Repository;
@@ -78,10 +80,37 @@ class PersistenceExceptionTranslationInterceptorTests extends PersistenceExcepti
given(invocation.proceed()).willThrow(exception);
assertThatThrownBy(() -> interceptor.invoke(invocation)).isSameAs(exception);
assertThat(callOrder).containsExactly(10, 20, 30);
}
@Test
void detectPersistenceExceptionTranslatorsOnShutdown() throws Throwable {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
bf.registerBeanDefinition("peti", new RootBeanDefinition(PersistenceExceptionTranslationInterceptor.class));
bf.registerBeanDefinition("pet", new RootBeanDefinition(ChainedPersistenceExceptionTranslator.class));
PersistenceExceptionTranslationInterceptor interceptor =
bf.getBean("peti", PersistenceExceptionTranslationInterceptor.class);
interceptor.setAlwaysTranslate(true);
RuntimeException exception = new RuntimeException();
MethodInvocation invocation = mock();
given(invocation.proceed()).willThrow(exception);
AtomicBoolean correctException = new AtomicBoolean(false);
bf.registerDisposableBean("disposable", () -> {
try {
interceptor.invoke(invocation);
}
catch (Throwable ex) {
correctException.set(ex == exception);
}
});
bf.destroySingletons();
assertThat(correctException).isTrue();
}
private static class CallOrderAwareExceptionTranslator implements PersistenceExceptionTranslator, Ordered {
@@ -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;
@@ -240,19 +240,16 @@ final class DefaultRestClient implements RestClient {
ResolvableType.forType(bodyType) + "] and content type [" + contentType + "]", cause);
if (observation != null) {
observation.error(restClientException);
observation.stop();
}
throw restClientException;
}
catch (RestClientException restClientException) {
if (observation != null) {
observation.error(restClientException);
}
throw restClientException;
}
finally {
if (observation != null) {
observation.stop();
}
throw restClientException;
}
}
@@ -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;
}
}
@@ -37,7 +37,6 @@ import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpSessionRequiredException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -115,7 +114,7 @@ public final class ModelFactory {
if (!container.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if (value == null) {
throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
throw new IllegalStateException("Expected session attribute '" + name + "'");
}
container.addAttribute(name, value);
}
@@ -89,7 +89,12 @@ public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
public ContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit) {
super(request);
int contentLength = request.getContentLength();
this.cachedContent = (contentLength > 0) ? new FastByteArrayOutputStream(contentLength) : new FastByteArrayOutputStream();
if (contentLength > 0) {
this.cachedContent = new FastByteArrayOutputStream(Math.min(contentLength, contentCacheLimit));
}
else {
this.cachedContent = new FastByteArrayOutputStream();
}
this.contentCacheLimit = contentCacheLimit;
}
@@ -36,7 +36,7 @@ import org.springframework.util.Assert;
public class DisconnectedClientHelper {
private static final Set<String> EXCEPTION_PHRASES =
Set.of("broken pipe", "connection reset by peer");
Set.of("broken pipe", "connection reset");
private static final Set<String> EXCEPTION_TYPE_NAMES =
Set.of("AbortedException", "ClientAbortException",
@@ -73,11 +73,12 @@ public class DisconnectedClientHelper {
/**
* Whether the given exception indicates the client has gone away.
* Known cases covered:
* <p>Known cases covered:
* <ul>
* <li>ClientAbortException or EOFException for Tomcat
* <li>EofException for Jetty
* <li>IOException "Broken pipe" or "connection reset by peer"
* <li>SocketException "Connection reset"
* </ul>
*/
public static boolean isClientDisconnectedException(Throwable ex) {
@@ -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
@@ -73,7 +73,6 @@ class RestClientObservationTests {
@BeforeEach
void setupEach() {
this.client = RestClient.builder()
.messageConverters(converters -> converters.add(0, this.converter))
.requestFactory(this.requestFactory)
@@ -267,6 +266,7 @@ class RestClientObservationTests {
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
TestObservationRegistryAssert.assertThat(this.observationRegistry).hasNumberOfObservationsWithNameEqualTo("http.client.requests",1);
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
.hasObservationWithNameEqualTo("http.client.requests").that();
}
@@ -50,6 +50,11 @@ class ServerHttpObservationFilterTests {
private final MockHttpServletResponse response = new MockHttpServletResponse();
@Test
void filterShouldNotProcessAsyncDispatch() {
assertThat(this.filter.shouldNotFilterAsyncDispatch()).isTrue();
}
@Test
void filterShouldFillObservationContext() throws Exception {
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
@@ -60,7 +65,7 @@ class ServerHttpObservationFilterTests {
assertThat(context.getCarrier()).isEqualTo(this.request);
assertThat(context.getResponse()).isEqualTo(this.response);
assertThat(context.getPathPattern()).isNull();
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
}
@Test
@@ -109,6 +114,16 @@ class ServerHttpObservationFilterTests {
.hasLowCardinalityKeyValue("status", "500");
}
@Test
void shouldCloseObservationAfterAsyncCompletion() throws Exception {
this.request.setAsyncSupported(true);
this.request.startAsync();
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
this.request.getAsyncContext().complete();
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
}
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
.hasObservationWithNameEqualTo("http.server.requests").that();
@@ -40,14 +40,14 @@ class HandlerMethodTests {
@Test
void shouldValidateArgsWithConstraintsDirectlyOnClass() {
Object target = new MyClass();
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addNames"), true);
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople", "addNames"), true);
testValidateArgs(target, List.of("addPerson", "getPerson", "getIntValue", "addPersonNotValidated"), false);
}
@Test
void shouldValidateArgsWithConstraintsOnInterface() {
Object target = new MyInterfaceImpl();
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons"), true);
testValidateArgs(target, List.of("addIntValue", "addPersonAndIntValue", "addPersons", "addPeople"), true);
testValidateArgs(target, List.of("addPerson", "addPersonNotValidated", "getPerson", "getIntValue"), false);
}
@@ -115,6 +115,9 @@ class HandlerMethodTests {
public void addPersons(@Valid List<Person> persons) {
}
public void addPeople(List<@Valid Person> persons) {
}
public void addNames(List<@NotEmpty String> names) {
}
@@ -144,6 +147,8 @@ class HandlerMethodTests {
void addPersons(@Valid List<Person> persons);
void addPeople(List<@Valid Person> persons);
void addPersonNotValidated(Person person);
@Valid
@@ -173,6 +178,10 @@ class HandlerMethodTests {
public void addPersons(List<Person> persons) {
}
@Override
public void addPeople(List<@Valid Person> persons) {
}
@Override
public void addPersonNotValidated(Person person) {
}
@@ -27,7 +27,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpSessionRequiredException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
@@ -43,7 +42,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@@ -151,7 +150,7 @@ class ModelFactoryTests {
void sessionAttributeNotPresent() throws Exception {
ModelFactory modelFactory = new ModelFactory(null, null, this.attributeHandler);
HandlerMethod handlerMethod = createHandlerMethod("handleSessionAttr", String.class);
assertThatExceptionOfType(HttpSessionRequiredException.class).isThrownBy(() ->
assertThatIllegalStateException().isThrownBy(() ->
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod));
// Now add attribute and try again
@@ -164,7 +163,7 @@ class ModelFactoryTests {
void sessionAttributeByType() throws Exception {
ModelFactory modelFactory = new ModelFactory(null, null, this.attributeHandler);
HandlerMethod handlerMethod = createHandlerMethod("handleTestBean", TestBean.class);
assertThatExceptionOfType(HttpSessionRequiredException.class).isThrownBy(() ->
assertThatIllegalStateException().isThrownBy(() ->
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod));
// Now add attribute and try again
@@ -17,12 +17,15 @@
package org.springframework.web.util;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
@@ -89,6 +92,19 @@ class ContentCachingRequestWrapperTests {
assertThat(wrapper.getContentAsString()).isEqualTo(new String("Hel".getBytes(CHARSET), CHARSET));
}
@Test
void shouldNotAllocateMoreThanCacheLimit() throws Exception {
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(createGetRequest("Hello World"), CONTENT_CACHE_LIMIT);
Field field = ReflectionUtils.findField(ContentCachingRequestWrapper.class, "cachedContent");
ReflectionUtils.makeAccessible(field);
FastByteArrayOutputStream cachedContent = (FastByteArrayOutputStream) ReflectionUtils.getField(field, wrapper);
field = ReflectionUtils.findField(FastByteArrayOutputStream.class, "initialBlockSize");
ReflectionUtils.makeAccessible(field);
int blockSize = (int) ReflectionUtils.getField(field, cachedContent);
assertThat(blockSize).isEqualTo(CONTENT_CACHE_LIMIT);
}
@Test
void cachedContentWithOverflow() throws Exception {
ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(